Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \
--bid 4335822 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4335822.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
diff --git a/android/graphics/AvoidXfermode.java b/android/graphics/AvoidXfermode.java
new file mode 100644
index 0000000..683c157
--- /dev/null
+++ b/android/graphics/AvoidXfermode.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2008 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 android.graphics;
+
+/**
+ * AvoidXfermode xfermode will draw the src everywhere except on top of the
+ * opColor or, depending on the Mode, draw only on top of the opColor.
+ *
+ * @removed
+ */
+@Deprecated
+public class AvoidXfermode extends Xfermode {
+
+ // these need to match the enum in AvoidXfermode.h on the native side
+ public enum Mode {
+ AVOID (0), //!< draw everywhere except on the opColor
+ TARGET (1); //!< draw only on top of the opColor
+
+ Mode(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ /** This xfermode draws, or doesn't draw, based on the destination's
+ * distance from an op-color.
+ *
+ * There are two modes, and each mode interprets a tolerance value.
+ *
+ * Avoid: In this mode, drawing is allowed only on destination pixels that
+ * are different from the op-color.
+ * Tolerance near 0: avoid any colors even remotely similar to the op-color
+ * Tolerance near 255: avoid only colors nearly identical to the op-color
+
+ * Target: In this mode, drawing only occurs on destination pixels that
+ * are similar to the op-color
+ * Tolerance near 0: draw only on colors that are nearly identical to the op-color
+ * Tolerance near 255: draw on any colors even remotely similar to the op-color
+ */
+ public AvoidXfermode(int opColor, int tolerance, Mode mode) {
+ if (tolerance < 0 || tolerance > 255) {
+ throw new IllegalArgumentException("tolerance must be 0..255");
+ }
+ }
+}
diff --git a/android/graphics/BaseCanvas.java b/android/graphics/BaseCanvas.java
new file mode 100644
index 0000000..2d8c717
--- /dev/null
+++ b/android/graphics/BaseCanvas.java
@@ -0,0 +1,633 @@
+/*
+ * Copyright (C) 2016 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 android.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.graphics.Canvas.VertexMode;
+import android.text.GraphicsOperations;
+import android.text.SpannableString;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.RecordingCanvas;
+
+/**
+ * This class is a base class for Canvas's drawing operations. Any modifications here
+ * should be accompanied by a similar modification to {@link RecordingCanvas}.
+ *
+ * The purpose of this class is to minimize the cost of deciding between regular JNI
+ * and @FastNative JNI to just the virtual call that Canvas already has.
+ *
+ * @hide
+ */
+public abstract class BaseCanvas {
+ /**
+ * Should only be assigned in constructors (or setBitmap if software canvas),
+ * freed by NativeAllocation.
+ */
+ protected long mNativeCanvasWrapper;
+
+ /**
+ * Used to determine when compatibility scaling is in effect.
+ */
+ protected int mScreenDensity = Bitmap.DENSITY_NONE;
+ protected int mDensity = Bitmap.DENSITY_NONE;
+ private boolean mAllowHwBitmapsInSwMode = false;
+
+ protected void throwIfCannotDraw(Bitmap bitmap) {
+ if (bitmap.isRecycled()) {
+ throw new RuntimeException("Canvas: trying to use a recycled bitmap " + bitmap);
+ }
+ if (!bitmap.isPremultiplied() && bitmap.getConfig() == Bitmap.Config.ARGB_8888 &&
+ bitmap.hasAlpha()) {
+ throw new RuntimeException("Canvas: trying to use a non-premultiplied bitmap "
+ + bitmap);
+ }
+ throwIfHwBitmapInSwMode(bitmap);
+ }
+
+ protected final static void checkRange(int length, int offset, int count) {
+ if ((offset | count) < 0 || offset + count > length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ }
+
+ public boolean isHardwareAccelerated() {
+ return false;
+ }
+
+ // ---------------------------------------------------------------------------
+ // Drawing methods
+ // These are also implemented in DisplayListCanvas so that we can
+ // selectively apply on them
+ // Everything below here is copy/pasted from Canvas.java
+ // The JNI registration is handled by android_view_Canvas.cpp
+ // ---------------------------------------------------------------------------
+
+ public void drawArc(float left, float top, float right, float bottom, float startAngle,
+ float sweepAngle, boolean useCenter, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawArc(mNativeCanvasWrapper, left, top, right, bottom, startAngle, sweepAngle,
+ useCenter, paint.getNativeInstance());
+ }
+
+ public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
+ @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ drawArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, useCenter,
+ paint);
+ }
+
+ public void drawARGB(int a, int r, int g, int b) {
+ drawColor(Color.argb(a, r, g, b));
+ }
+
+ public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint) {
+ throwIfCannotDraw(bitmap);
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawBitmap(mNativeCanvasWrapper, bitmap, left, top,
+ paint != null ? paint.getNativeInstance() : 0, mDensity, mScreenDensity,
+ bitmap.mDensity);
+ }
+
+ public void drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, @Nullable Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawBitmapMatrix(mNativeCanvasWrapper, bitmap, matrix.ni(),
+ paint != null ? paint.getNativeInstance() : 0);
+ }
+
+ public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,
+ @Nullable Paint paint) {
+ if (dst == null) {
+ throw new NullPointerException();
+ }
+ throwIfCannotDraw(bitmap);
+ throwIfHasHwBitmapInSwMode(paint);
+ final long nativePaint = paint == null ? 0 : paint.getNativeInstance();
+
+ int left, top, right, bottom;
+ if (src == null) {
+ left = top = 0;
+ right = bitmap.getWidth();
+ bottom = bitmap.getHeight();
+ } else {
+ left = src.left;
+ right = src.right;
+ top = src.top;
+ bottom = src.bottom;
+ }
+
+ nDrawBitmap(mNativeCanvasWrapper, bitmap, left, top, right, bottom,
+ dst.left, dst.top, dst.right, dst.bottom, nativePaint, mScreenDensity,
+ bitmap.mDensity);
+ }
+
+ public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst,
+ @Nullable Paint paint) {
+ if (dst == null) {
+ throw new NullPointerException();
+ }
+ throwIfCannotDraw(bitmap);
+ throwIfHasHwBitmapInSwMode(paint);
+ final long nativePaint = paint == null ? 0 : paint.getNativeInstance();
+
+ float left, top, right, bottom;
+ if (src == null) {
+ left = top = 0;
+ right = bitmap.getWidth();
+ bottom = bitmap.getHeight();
+ } else {
+ left = src.left;
+ right = src.right;
+ top = src.top;
+ bottom = src.bottom;
+ }
+
+ nDrawBitmap(mNativeCanvasWrapper, bitmap, left, top, right, bottom,
+ dst.left, dst.top, dst.right, dst.bottom, nativePaint, mScreenDensity,
+ bitmap.mDensity);
+ }
+
+ @Deprecated
+ public void drawBitmap(@NonNull int[] colors, int offset, int stride, float x, float y,
+ int width, int height, boolean hasAlpha, @Nullable Paint paint) {
+ // check for valid input
+ if (width < 0) {
+ throw new IllegalArgumentException("width must be >= 0");
+ }
+ if (height < 0) {
+ throw new IllegalArgumentException("height must be >= 0");
+ }
+ if (Math.abs(stride) < width) {
+ throw new IllegalArgumentException("abs(stride) must be >= width");
+ }
+ int lastScanline = offset + (height - 1) * stride;
+ int length = colors.length;
+ if (offset < 0 || (offset + width > length) || lastScanline < 0
+ || (lastScanline + width > length)) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ throwIfHasHwBitmapInSwMode(paint);
+ // quick escape if there's nothing to draw
+ if (width == 0 || height == 0) {
+ return;
+ }
+ // punch down to native for the actual draw
+ nDrawBitmap(mNativeCanvasWrapper, colors, offset, stride, x, y, width, height, hasAlpha,
+ paint != null ? paint.getNativeInstance() : 0);
+ }
+
+ @Deprecated
+ public void drawBitmap(@NonNull int[] colors, int offset, int stride, int x, int y,
+ int width, int height, boolean hasAlpha, @Nullable Paint paint) {
+ // call through to the common float version
+ drawBitmap(colors, offset, stride, (float) x, (float) y, width, height,
+ hasAlpha, paint);
+ }
+
+ public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight,
+ @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
+ @Nullable Paint paint) {
+ if ((meshWidth | meshHeight | vertOffset | colorOffset) < 0) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ throwIfHasHwBitmapInSwMode(paint);
+ if (meshWidth == 0 || meshHeight == 0) {
+ return;
+ }
+ int count = (meshWidth + 1) * (meshHeight + 1);
+ // we mul by 2 since we need two floats per vertex
+ checkRange(verts.length, vertOffset, count * 2);
+ if (colors != null) {
+ // no mul by 2, since we need only 1 color per vertex
+ checkRange(colors.length, colorOffset, count);
+ }
+ nDrawBitmapMesh(mNativeCanvasWrapper, bitmap, meshWidth, meshHeight,
+ verts, vertOffset, colors, colorOffset,
+ paint != null ? paint.getNativeInstance() : 0);
+ }
+
+ public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawCircle(mNativeCanvasWrapper, cx, cy, radius, paint.getNativeInstance());
+ }
+
+ public void drawColor(@ColorInt int color) {
+ nDrawColor(mNativeCanvasWrapper, color, PorterDuff.Mode.SRC_OVER.nativeInt);
+ }
+
+ public void drawColor(@ColorInt int color, @NonNull PorterDuff.Mode mode) {
+ nDrawColor(mNativeCanvasWrapper, color, mode.nativeInt);
+ }
+
+ public void drawLine(float startX, float startY, float stopX, float stopY,
+ @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawLine(mNativeCanvasWrapper, startX, startY, stopX, stopY, paint.getNativeInstance());
+ }
+
+ public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count,
+ @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawLines(mNativeCanvasWrapper, pts, offset, count, paint.getNativeInstance());
+ }
+
+ public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ drawLines(pts, 0, pts.length, paint);
+ }
+
+ public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawOval(mNativeCanvasWrapper, left, top, right, bottom, paint.getNativeInstance());
+ }
+
+ public void drawOval(@NonNull RectF oval, @NonNull Paint paint) {
+ if (oval == null) {
+ throw new NullPointerException();
+ }
+ throwIfHasHwBitmapInSwMode(paint);
+ drawOval(oval.left, oval.top, oval.right, oval.bottom, paint);
+ }
+
+ public void drawPaint(@NonNull Paint paint) {
+ nDrawPaint(mNativeCanvasWrapper, paint.getNativeInstance());
+ }
+
+ public void drawPatch(@NonNull NinePatch patch, @NonNull Rect dst, @Nullable Paint paint) {
+ Bitmap bitmap = patch.getBitmap();
+ throwIfCannotDraw(bitmap);
+ throwIfHasHwBitmapInSwMode(paint);
+ final long nativePaint = paint == null ? 0 : paint.getNativeInstance();
+ nDrawNinePatch(mNativeCanvasWrapper, bitmap.getNativeInstance(), patch.mNativeChunk,
+ dst.left, dst.top, dst.right, dst.bottom, nativePaint,
+ mDensity, patch.getDensity());
+ }
+
+ public void drawPatch(@NonNull NinePatch patch, @NonNull RectF dst, @Nullable Paint paint) {
+ Bitmap bitmap = patch.getBitmap();
+ throwIfCannotDraw(bitmap);
+ throwIfHasHwBitmapInSwMode(paint);
+ final long nativePaint = paint == null ? 0 : paint.getNativeInstance();
+ nDrawNinePatch(mNativeCanvasWrapper, bitmap.getNativeInstance(), patch.mNativeChunk,
+ dst.left, dst.top, dst.right, dst.bottom, nativePaint,
+ mDensity, patch.getDensity());
+ }
+
+ public void drawPath(@NonNull Path path, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ if (path.isSimplePath && path.rects != null) {
+ nDrawRegion(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance());
+ } else {
+ nDrawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance());
+ }
+ }
+
+ public void drawPoint(float x, float y, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawPoint(mNativeCanvasWrapper, x, y, paint.getNativeInstance());
+ }
+
+ public void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count,
+ @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawPoints(mNativeCanvasWrapper, pts, offset, count, paint.getNativeInstance());
+ }
+
+ public void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ drawPoints(pts, 0, pts.length, paint);
+ }
+
+ @Deprecated
+ public void drawPosText(@NonNull char[] text, int index, int count,
+ @NonNull @Size(multiple = 2) float[] pos,
+ @NonNull Paint paint) {
+ if (index < 0 || index + count > text.length || count * 2 > pos.length) {
+ throw new IndexOutOfBoundsException();
+ }
+ throwIfHasHwBitmapInSwMode(paint);
+ for (int i = 0; i < count; i++) {
+ drawText(text, index + i, 1, pos[i * 2], pos[i * 2 + 1], paint);
+ }
+ }
+
+ @Deprecated
+ public void drawPosText(@NonNull String text, @NonNull @Size(multiple = 2) float[] pos,
+ @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ drawPosText(text.toCharArray(), 0, text.length(), pos, paint);
+ }
+
+ public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawRect(mNativeCanvasWrapper, left, top, right, bottom, paint.getNativeInstance());
+ }
+
+ public void drawRect(@NonNull Rect r, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ drawRect(r.left, r.top, r.right, r.bottom, paint);
+ }
+
+ public void drawRect(@NonNull RectF rect, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawRect(mNativeCanvasWrapper,
+ rect.left, rect.top, rect.right, rect.bottom, paint.getNativeInstance());
+ }
+
+ public void drawRGB(int r, int g, int b) {
+ drawColor(Color.rgb(r, g, b));
+ }
+
+ public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,
+ @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawRoundRect(mNativeCanvasWrapper, left, top, right, bottom, rx, ry,
+ paint.getNativeInstance());
+ }
+
+ public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ drawRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint);
+ }
+
+ public void drawText(@NonNull char[] text, int index, int count, float x, float y,
+ @NonNull Paint paint) {
+ if ((index | count | (index + count) |
+ (text.length - index - count)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawText(mNativeCanvasWrapper, text, index, count, x, y, paint.mBidiFlags,
+ paint.getNativeInstance());
+ }
+
+ public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
+ @NonNull Paint paint) {
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ throwIfHasHwBitmapInSwMode(paint);
+ if (text instanceof String || text instanceof SpannedString ||
+ text instanceof SpannableString) {
+ nDrawText(mNativeCanvasWrapper, text.toString(), start, end, x, y,
+ paint.mBidiFlags, paint.getNativeInstance());
+ } else if (text instanceof GraphicsOperations) {
+ ((GraphicsOperations) text).drawText(this, start, end, x, y,
+ paint);
+ } else {
+ char[] buf = TemporaryBuffer.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ nDrawText(mNativeCanvasWrapper, buf, 0, end - start, x, y,
+ paint.mBidiFlags, paint.getNativeInstance());
+ TemporaryBuffer.recycle(buf);
+ }
+ }
+
+ public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags,
+ paint.getNativeInstance());
+ }
+
+ public void drawText(@NonNull String text, int start, int end, float x, float y,
+ @NonNull Paint paint) {
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawText(mNativeCanvasWrapper, text, start, end, x, y, paint.mBidiFlags,
+ paint.getNativeInstance());
+ }
+
+ public void drawTextOnPath(@NonNull char[] text, int index, int count, @NonNull Path path,
+ float hOffset, float vOffset, @NonNull Paint paint) {
+ if (index < 0 || index + count > text.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawTextOnPath(mNativeCanvasWrapper, text, index, count,
+ path.readOnlyNI(), hOffset, vOffset,
+ paint.mBidiFlags, paint.getNativeInstance());
+ }
+
+ public void drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset,
+ float vOffset, @NonNull Paint paint) {
+ if (text.length() > 0) {
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawTextOnPath(mNativeCanvasWrapper, text, path.readOnlyNI(), hOffset, vOffset,
+ paint.mBidiFlags, paint.getNativeInstance());
+ }
+ }
+
+ public void drawTextRun(@NonNull char[] text, int index, int count, int contextIndex,
+ int contextCount, float x, float y, boolean isRtl, @NonNull Paint paint) {
+
+ if (text == null) {
+ throw new NullPointerException("text is null");
+ }
+ if (paint == null) {
+ throw new NullPointerException("paint is null");
+ }
+ if ((index | count | contextIndex | contextCount | index - contextIndex
+ | (contextIndex + contextCount) - (index + count)
+ | text.length - (contextIndex + contextCount)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawTextRun(mNativeCanvasWrapper, text, index, count, contextIndex, contextCount,
+ x, y, isRtl, paint.getNativeInstance());
+ }
+
+ public void drawTextRun(@NonNull CharSequence text, int start, int end, int contextStart,
+ int contextEnd, float x, float y, boolean isRtl, @NonNull Paint paint) {
+
+ if (text == null) {
+ throw new NullPointerException("text is null");
+ }
+ if (paint == null) {
+ throw new NullPointerException("paint is null");
+ }
+ if ((start | end | contextStart | contextEnd | start - contextStart | end - start
+ | contextEnd - end | text.length() - contextEnd) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ throwIfHasHwBitmapInSwMode(paint);
+ if (text instanceof String || text instanceof SpannedString ||
+ text instanceof SpannableString) {
+ nDrawTextRun(mNativeCanvasWrapper, text.toString(), start, end, contextStart,
+ contextEnd, x, y, isRtl, paint.getNativeInstance());
+ } else if (text instanceof GraphicsOperations) {
+ ((GraphicsOperations) text).drawTextRun(this, start, end,
+ contextStart, contextEnd, x, y, isRtl, paint);
+ } else {
+ int contextLen = contextEnd - contextStart;
+ int len = end - start;
+ char[] buf = TemporaryBuffer.obtain(contextLen);
+ TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ nDrawTextRun(mNativeCanvasWrapper, buf, start - contextStart, len,
+ 0, contextLen, x, y, isRtl, paint.getNativeInstance());
+ TemporaryBuffer.recycle(buf);
+ }
+ }
+
+ public void drawVertices(@NonNull VertexMode mode, int vertexCount, @NonNull float[] verts,
+ int vertOffset, @Nullable float[] texs, int texOffset, @Nullable int[] colors,
+ int colorOffset, @Nullable short[] indices, int indexOffset, int indexCount,
+ @NonNull Paint paint) {
+ checkRange(verts.length, vertOffset, vertexCount);
+ if (isHardwareAccelerated()) {
+ return;
+ }
+ if (texs != null) {
+ checkRange(texs.length, texOffset, vertexCount);
+ }
+ if (colors != null) {
+ checkRange(colors.length, colorOffset, vertexCount / 2);
+ }
+ if (indices != null) {
+ checkRange(indices.length, indexOffset, indexCount);
+ }
+ throwIfHasHwBitmapInSwMode(paint);
+ nDrawVertices(mNativeCanvasWrapper, mode.nativeInt, vertexCount, verts,
+ vertOffset, texs, texOffset, colors, colorOffset,
+ indices, indexOffset, indexCount, paint.getNativeInstance());
+ }
+
+ /**
+ * @hide
+ */
+ public void setHwBitmapsInSwModeEnabled(boolean enabled) {
+ mAllowHwBitmapsInSwMode = enabled;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isHwBitmapsInSwModeEnabled() {
+ return mAllowHwBitmapsInSwMode;
+ }
+
+ private void throwIfHwBitmapInSwMode(Bitmap bitmap) {
+ if (!mAllowHwBitmapsInSwMode && !isHardwareAccelerated()
+ && bitmap.getConfig() == Bitmap.Config.HARDWARE) {
+ throw new IllegalStateException("Software rendering doesn't support hardware bitmaps");
+ }
+ }
+
+ private void throwIfHasHwBitmapInSwMode(Paint p) {
+ if (isHardwareAccelerated() || p == null) {
+ return;
+ }
+ throwIfHasHwBitmapInSwMode(p.getShader());
+ }
+
+ private void throwIfHasHwBitmapInSwMode(Shader shader) {
+ if (shader == null) {
+ return;
+ }
+ if (shader instanceof BitmapShader) {
+ throwIfHwBitmapInSwMode(((BitmapShader) shader).mBitmap);
+ }
+ if (shader instanceof ComposeShader) {
+ throwIfHasHwBitmapInSwMode(((ComposeShader) shader).mShaderA);
+ throwIfHasHwBitmapInSwMode(((ComposeShader) shader).mShaderB);
+ }
+ }
+
+ private static native void nDrawBitmap(long nativeCanvas, Bitmap bitmap, float left, float top,
+ long nativePaintOrZero, int canvasDensity, int screenDensity, int bitmapDensity);
+
+ private static native void nDrawBitmap(long nativeCanvas, Bitmap bitmap, float srcLeft,
+ float srcTop,
+ float srcRight, float srcBottom, float dstLeft, float dstTop, float dstRight,
+ float dstBottom, long nativePaintOrZero, int screenDensity, int bitmapDensity);
+
+ private static native void nDrawBitmap(long nativeCanvas, int[] colors, int offset, int stride,
+ float x, float y, int width, int height, boolean hasAlpha, long nativePaintOrZero);
+
+ private static native void nDrawColor(long nativeCanvas, int color, int mode);
+
+ private static native void nDrawPaint(long nativeCanvas, long nativePaint);
+
+ private static native void nDrawPoint(long canvasHandle, float x, float y, long paintHandle);
+
+ private static native void nDrawPoints(long canvasHandle, float[] pts, int offset, int count,
+ long paintHandle);
+
+ private static native void nDrawLine(long nativeCanvas, float startX, float startY, float stopX,
+ float stopY, long nativePaint);
+
+ private static native void nDrawLines(long canvasHandle, float[] pts, int offset, int count,
+ long paintHandle);
+
+ private static native void nDrawRect(long nativeCanvas, float left, float top, float right,
+ float bottom, long nativePaint);
+
+ private static native void nDrawOval(long nativeCanvas, float left, float top, float right,
+ float bottom, long nativePaint);
+
+ private static native void nDrawCircle(long nativeCanvas, float cx, float cy, float radius,
+ long nativePaint);
+
+ private static native void nDrawArc(long nativeCanvas, float left, float top, float right,
+ float bottom, float startAngle, float sweep, boolean useCenter, long nativePaint);
+
+ private static native void nDrawRoundRect(long nativeCanvas, float left, float top, float right,
+ float bottom, float rx, float ry, long nativePaint);
+
+ private static native void nDrawPath(long nativeCanvas, long nativePath, long nativePaint);
+
+ private static native void nDrawRegion(long nativeCanvas, long nativeRegion, long nativePaint);
+
+ private static native void nDrawNinePatch(long nativeCanvas, long nativeBitmap, long ninePatch,
+ float dstLeft, float dstTop, float dstRight, float dstBottom, long nativePaintOrZero,
+ int screenDensity, int bitmapDensity);
+
+ private static native void nDrawBitmapMatrix(long nativeCanvas, Bitmap bitmap,
+ long nativeMatrix, long nativePaint);
+
+ private static native void nDrawBitmapMesh(long nativeCanvas, Bitmap bitmap, int meshWidth,
+ int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset,
+ long nativePaint);
+
+ private static native void nDrawVertices(long nativeCanvas, int mode, int n, float[] verts,
+ int vertOffset, float[] texs, int texOffset, int[] colors, int colorOffset,
+ short[] indices, int indexOffset, int indexCount, long nativePaint);
+
+ private static native void nDrawText(long nativeCanvas, char[] text, int index, int count,
+ float x, float y, int flags, long nativePaint);
+
+ private static native void nDrawText(long nativeCanvas, String text, int start, int end,
+ float x, float y, int flags, long nativePaint);
+
+ private static native void nDrawTextRun(long nativeCanvas, String text, int start, int end,
+ int contextStart, int contextEnd, float x, float y, boolean isRtl, long nativePaint);
+
+ private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count,
+ int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint);
+
+ private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count,
+ long nativePath, float hOffset, float vOffset, int bidiFlags, long nativePaint);
+
+ private static native void nDrawTextOnPath(long nativeCanvas, String text, long nativePath,
+ float hOffset, float vOffset, int flags, long nativePaint);
+}
diff --git a/android/graphics/BaseCanvas_Delegate.java b/android/graphics/BaseCanvas_Delegate.java
new file mode 100644
index 0000000..89fccc7
--- /dev/null
+++ b/android/graphics/BaseCanvas_Delegate.java
@@ -0,0 +1,757 @@
+/*
+ * Copyright (C) 2016 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.layoutlib.bridge.impl.GcSnapshot;
+import com.android.layoutlib.bridge.impl.PorterDuffUtility;
+import com.android.ninepatch.NinePatchChunk;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Arc2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+
+public class BaseCanvas_Delegate {
+ // ---- delegate manager ----
+ protected static DelegateManager<BaseCanvas_Delegate> sManager =
+ new DelegateManager<>(BaseCanvas_Delegate.class);
+
+ // ---- delegate helper data ----
+ private final static boolean[] sBoolOut = new boolean[1];
+
+
+ // ---- delegate data ----
+ protected Bitmap_Delegate mBitmap;
+ protected GcSnapshot mSnapshot;
+
+ // ---- Public Helper methods ----
+
+ protected BaseCanvas_Delegate(Bitmap_Delegate bitmap) {
+ mSnapshot = GcSnapshot.createDefaultSnapshot(mBitmap = bitmap);
+ }
+
+ protected BaseCanvas_Delegate() {
+ mSnapshot = GcSnapshot.createDefaultSnapshot(null /*image*/);
+ }
+
+ /**
+ * Disposes of the {@link Graphics2D} stack.
+ */
+ protected void dispose() {
+ mSnapshot.dispose();
+ }
+
+ /**
+ * Returns the current {@link Graphics2D} used to draw.
+ */
+ public GcSnapshot getSnapshot() {
+ return mSnapshot;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawBitmap(long nativeCanvas, Bitmap bitmap, float left, float top,
+ long nativePaintOrZero, int canvasDensity, int screenDensity, int bitmapDensity) {
+ // get the delegate from the native int.
+ Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(bitmap);
+ if (bitmapDelegate == null) {
+ return;
+ }
+
+ BufferedImage image = bitmapDelegate.getImage();
+ float right = left + image.getWidth();
+ float bottom = top + image.getHeight();
+
+ drawBitmap(nativeCanvas, bitmapDelegate, nativePaintOrZero,
+ 0, 0, image.getWidth(), image.getHeight(),
+ (int)left, (int)top, (int)right, (int)bottom);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawBitmap(long nativeCanvas, Bitmap bitmap, float srcLeft, float srcTop,
+ float srcRight, float srcBottom, float dstLeft, float dstTop, float dstRight,
+ float dstBottom, long nativePaintOrZero, int screenDensity, int bitmapDensity) {
+ // get the delegate from the native int.
+ Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(bitmap);
+ if (bitmapDelegate == null) {
+ return;
+ }
+
+ drawBitmap(nativeCanvas, bitmapDelegate, nativePaintOrZero, (int) srcLeft, (int) srcTop,
+ (int) srcRight, (int) srcBottom, (int) dstLeft, (int) dstTop, (int) dstRight,
+ (int) dstBottom);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawBitmap(long nativeCanvas, int[] colors, int offset, int stride,
+ final float x, final float y, int width, int height, boolean hasAlpha,
+ long nativePaintOrZero) {
+ // create a temp BufferedImage containing the content.
+ final BufferedImage image = new BufferedImage(width, height,
+ hasAlpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
+ image.setRGB(0, 0, width, height, colors, offset, stride);
+
+ draw(nativeCanvas, nativePaintOrZero, true /*compositeOnly*/, false /*forceSrcMode*/,
+ (graphics, paint) -> {
+ if (paint != null && paint.isFilterBitmap()) {
+ graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+ RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ }
+
+ graphics.drawImage(image, (int) x, (int) y, null);
+ });
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawColor(long nativeCanvas, final int color, final int mode) {
+ // get the delegate from the native int.
+ BaseCanvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ final int w = canvasDelegate.mBitmap.getImage().getWidth();
+ final int h = canvasDelegate.mBitmap.getImage().getHeight();
+ draw(nativeCanvas, (graphics, paint) -> {
+ // reset its transform just in case
+ graphics.setTransform(new AffineTransform());
+
+ // set the color
+ graphics.setColor(new java.awt.Color(color, true /*alpha*/));
+
+ Composite composite = PorterDuffUtility.getComposite(
+ PorterDuffUtility.getPorterDuffMode(mode), 0xFF);
+ if (composite != null) {
+ graphics.setComposite(composite);
+ }
+
+ graphics.fillRect(0, 0, w, h);
+ });
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawPaint(long nativeCanvas, long paint) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Canvas.drawPaint is not supported.", null, null /*data*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawPoint(long nativeCanvas, float x, float y,
+ long nativePaint) {
+ // TODO: need to support the attribute (e.g. stroke width) of paint
+ draw(nativeCanvas, nativePaint, false /*compositeOnly*/, false /*forceSrcMode*/,
+ (graphics, paintDelegate) -> graphics.fillRect((int)x, (int)y, 1, 1));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawPoints(long nativeCanvas, float[] pts, int offset, int count,
+ long nativePaint) {
+ if (offset < 0 || count < 0 || offset + count > pts.length) {
+ throw new IllegalArgumentException("Invalid argument set");
+ }
+ // ignore the last point if the count is odd (It means it is not paired).
+ count = (count >> 1) << 1;
+ for (int i = offset; i < offset + count; i += 2) {
+ nDrawPoint(nativeCanvas, pts[i], pts[i + 1], nativePaint);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawLine(long nativeCanvas,
+ final float startX, final float startY, final float stopX, final float stopY,
+ long paint) {
+ draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
+ (graphics, paintDelegate) -> graphics.drawLine((int)startX, (int)startY, (int)stopX, (int)stopY));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawLines(long nativeCanvas,
+ final float[] pts, final int offset, final int count,
+ long nativePaint) {
+ draw(nativeCanvas, nativePaint, false /*compositeOnly*/,
+ false /*forceSrcMode*/, (graphics, paintDelegate) -> {
+ for (int i = 0; i < count; i += 4) {
+ graphics.drawLine((int) pts[i + offset], (int) pts[i + offset + 1],
+ (int) pts[i + offset + 2], (int) pts[i + offset + 3]);
+ }
+ });
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawRect(long nativeCanvas,
+ final float left, final float top, final float right, final float bottom, long paint) {
+
+ draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
+ (graphics, paintDelegate) -> {
+ int style = paintDelegate.getStyle();
+
+ // draw
+ if (style == Paint.Style.FILL.nativeInt ||
+ style == Paint.Style.FILL_AND_STROKE.nativeInt) {
+ graphics.fillRect((int)left, (int)top,
+ (int)(right-left), (int)(bottom-top));
+ }
+
+ if (style == Paint.Style.STROKE.nativeInt ||
+ style == Paint.Style.FILL_AND_STROKE.nativeInt) {
+ graphics.drawRect((int)left, (int)top,
+ (int)(right-left), (int)(bottom-top));
+ }
+ });
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawOval(long nativeCanvas, final float left,
+ final float top, final float right, final float bottom, long paint) {
+ if (right > left && bottom > top) {
+ draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
+ (graphics, paintDelegate) -> {
+ int style = paintDelegate.getStyle();
+
+ // draw
+ if (style == Paint.Style.FILL.nativeInt ||
+ style == Paint.Style.FILL_AND_STROKE.nativeInt) {
+ graphics.fillOval((int)left, (int)top,
+ (int)(right - left), (int)(bottom - top));
+ }
+
+ if (style == Paint.Style.STROKE.nativeInt ||
+ style == Paint.Style.FILL_AND_STROKE.nativeInt) {
+ graphics.drawOval((int)left, (int)top,
+ (int)(right - left), (int)(bottom - top));
+ }
+ });
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawCircle(long nativeCanvas,
+ float cx, float cy, float radius, long paint) {
+ nDrawOval(nativeCanvas,
+ cx - radius, cy - radius, cx + radius, cy + radius,
+ paint);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawArc(long nativeCanvas,
+ final float left, final float top, final float right, final float bottom,
+ final float startAngle, final float sweep,
+ final boolean useCenter, long paint) {
+ if (right > left && bottom > top) {
+ draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
+ (graphics, paintDelegate) -> {
+ int style = paintDelegate.getStyle();
+
+ Arc2D.Float arc = new Arc2D.Float(
+ left, top, right - left, bottom - top,
+ -startAngle, -sweep,
+ useCenter ? Arc2D.PIE : Arc2D.OPEN);
+
+ // draw
+ if (style == Paint.Style.FILL.nativeInt ||
+ style == Paint.Style.FILL_AND_STROKE.nativeInt) {
+ graphics.fill(arc);
+ }
+
+ if (style == Paint.Style.STROKE.nativeInt ||
+ style == Paint.Style.FILL_AND_STROKE.nativeInt) {
+ graphics.draw(arc);
+ }
+ });
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawRoundRect(long nativeCanvas,
+ final float left, final float top, final float right, final float bottom,
+ final float rx, final float ry, long paint) {
+ draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
+ (graphics, paintDelegate) -> {
+ int style = paintDelegate.getStyle();
+
+ // draw
+ if (style == Paint.Style.FILL.nativeInt ||
+ style == Paint.Style.FILL_AND_STROKE.nativeInt) {
+ graphics.fillRoundRect(
+ (int)left, (int)top,
+ (int)(right - left), (int)(bottom - top),
+ 2 * (int)rx, 2 * (int)ry);
+ }
+
+ if (style == Paint.Style.STROKE.nativeInt ||
+ style == Paint.Style.FILL_AND_STROKE.nativeInt) {
+ graphics.drawRoundRect(
+ (int)left, (int)top,
+ (int)(right - left), (int)(bottom - top),
+ 2 * (int)rx, 2 * (int)ry);
+ }
+ });
+ }
+
+ @LayoutlibDelegate
+ public static void nDrawPath(long nativeCanvas, long path, long paint) {
+ final Path_Delegate pathDelegate = Path_Delegate.getDelegate(path);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
+ (graphics, paintDelegate) -> {
+ Shape shape = pathDelegate.getJavaShape();
+ Rectangle2D bounds = shape.getBounds2D();
+ if (bounds.isEmpty()) {
+ // Apple JRE 1.6 doesn't like drawing empty shapes.
+ // http://b.android.com/178278
+
+ if (pathDelegate.isEmpty()) {
+ // This means that the path doesn't have any lines or curves so
+ // nothing to draw.
+ return;
+ }
+
+ // The stroke width is not consider for the size of the bounds so,
+ // for example, a horizontal line, would be considered as an empty
+ // rectangle.
+ // If the strokeWidth is not 0, we use it to consider the size of the
+ // path as well.
+ float strokeWidth = paintDelegate.getStrokeWidth();
+ if (strokeWidth <= 0.0f) {
+ return;
+ }
+ bounds.setRect(bounds.getX(), bounds.getY(),
+ Math.max(strokeWidth, bounds.getWidth()),
+ Math.max(strokeWidth, bounds.getHeight()));
+ }
+
+ int style = paintDelegate.getStyle();
+
+ if (style == Paint.Style.FILL.nativeInt ||
+ style == Paint.Style.FILL_AND_STROKE.nativeInt) {
+ graphics.fill(shape);
+ }
+
+ if (style == Paint.Style.STROKE.nativeInt ||
+ style == Paint.Style.FILL_AND_STROKE.nativeInt) {
+ graphics.draw(shape);
+ }
+ });
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawRegion(long nativeCanvas, long nativeRegion,
+ long nativePaint) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Some canvas paths may not be drawn", null, null);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawNinePatch(long nativeCanvas, long nativeBitmap, long ninePatch,
+ final float dstLeft, final float dstTop, final float dstRight, final float dstBottom,
+ long nativePaintOrZero, final int screenDensity, final int bitmapDensity) {
+
+ // get the delegate from the native int.
+ final Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(nativeBitmap);
+ if (bitmapDelegate == null) {
+ return;
+ }
+
+ byte[] c = NinePatch_Delegate.getChunk(ninePatch);
+ if (c == null) {
+ // not a 9-patch?
+ BufferedImage image = bitmapDelegate.getImage();
+ drawBitmap(nativeCanvas, bitmapDelegate, nativePaintOrZero, 0, 0, image.getWidth(),
+ image.getHeight(), (int) dstLeft, (int) dstTop, (int) dstRight,
+ (int) dstBottom);
+ return;
+ }
+
+ final NinePatchChunk chunkObject = NinePatch_Delegate.getChunk(c);
+ if (chunkObject == null) {
+ return;
+ }
+
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ // this one can be null
+ Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(nativePaintOrZero);
+
+ canvasDelegate.getSnapshot().draw(new GcSnapshot.Drawable() {
+ @Override
+ public void draw(Graphics2D graphics, Paint_Delegate paint) {
+ chunkObject.draw(bitmapDelegate.getImage(), graphics, (int) dstLeft, (int) dstTop,
+ (int) (dstRight - dstLeft), (int) (dstBottom - dstTop), screenDensity,
+ bitmapDensity);
+ }
+ }, paintDelegate, true, false);
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawBitmapMatrix(long nCanvas, Bitmap bitmap,
+ long nMatrix, long nPaint) {
+ // get the delegate from the native int.
+ BaseCanvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ // get the delegate from the native int, which can be null
+ Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(nPaint);
+
+ // get the delegate from the native int.
+ Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(bitmap);
+ if (bitmapDelegate == null) {
+ return;
+ }
+
+ final BufferedImage image = getImageToDraw(bitmapDelegate, paintDelegate, sBoolOut);
+
+ Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(nMatrix);
+ if (matrixDelegate == null) {
+ return;
+ }
+
+ final AffineTransform mtx = matrixDelegate.getAffineTransform();
+
+ canvasDelegate.getSnapshot().draw((graphics, paint) -> {
+ if (paint != null && paint.isFilterBitmap()) {
+ graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+ RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ }
+
+ //FIXME add support for canvas, screen and bitmap densities.
+ graphics.drawImage(image, mtx, null);
+ }, paintDelegate, true /*compositeOnly*/, false /*forceSrcMode*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawBitmapMesh(long nCanvas, Bitmap bitmap,
+ int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors,
+ int colorOffset, long nPaint) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Canvas.drawBitmapMesh is not supported.", null, null /*data*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawVertices(long nCanvas, int mode, int n,
+ float[] verts, int vertOffset,
+ float[] texs, int texOffset,
+ int[] colors, int colorOffset,
+ short[] indices, int indexOffset,
+ int indexCount, long nPaint) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Canvas.drawVertices is not supported.", null, null /*data*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawText(long nativeCanvas, char[] text, int index, int count,
+ float startX, float startY, int flags, long paint) {
+ drawText(nativeCanvas, text, index, count, startX, startY, flags,
+ paint);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawText(long nativeCanvas, String text,
+ int start, int end, float x, float y, final int flags, long paint) {
+ int count = end - start;
+ char[] buffer = TemporaryBuffer.obtain(count);
+ TextUtils.getChars(text, start, end, buffer, 0);
+
+ nDrawText(nativeCanvas, buffer, 0, count, x, y, flags, paint);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawTextRun(long nativeCanvas, String text,
+ int start, int end, int contextStart, int contextEnd,
+ float x, float y, boolean isRtl, long paint) {
+ int count = end - start;
+ char[] buffer = TemporaryBuffer.obtain(count);
+ TextUtils.getChars(text, start, end, buffer, 0);
+
+ drawText(nativeCanvas, buffer, 0, count, x, y, isRtl ? Paint.BIDI_RTL : Paint.BIDI_LTR,
+ paint);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawTextRun(long nativeCanvas, char[] text,
+ int start, int count, int contextStart, int contextCount,
+ float x, float y, boolean isRtl, long paint) {
+ drawText(nativeCanvas, text, start, count, x, y, isRtl ? Paint.BIDI_RTL : Paint.BIDI_LTR, paint);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawTextOnPath(long nativeCanvas,
+ char[] text, int index,
+ int count, long path,
+ float hOffset,
+ float vOffset, int bidiFlags,
+ long paint) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Canvas.drawTextOnPath is not supported.", null, null /*data*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nDrawTextOnPath(long nativeCanvas,
+ String text, long path,
+ float hOffset,
+ float vOffset,
+ int bidiFlags, long paint) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Canvas.drawTextOnPath is not supported.", null, null /*data*/);
+ }
+
+ // ---- Private delegate/helper methods ----
+
+ /**
+ * Executes a {@link GcSnapshot.Drawable} with a given canvas and paint.
+ * <p>Note that the drawable may actually be executed several times if there are
+ * layers involved (see {@link #saveLayer(RectF, Paint_Delegate, int)}.
+ */
+ private static void draw(long nCanvas, long nPaint, boolean compositeOnly, boolean forceSrcMode,
+ GcSnapshot.Drawable drawable) {
+ // get the delegate from the native int.
+ BaseCanvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ // get the paint which can be null if nPaint is 0;
+ Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(nPaint);
+
+ canvasDelegate.getSnapshot().draw(drawable, paintDelegate, compositeOnly, forceSrcMode);
+ }
+
+ /**
+ * Executes a {@link GcSnapshot.Drawable} with a given canvas. No paint object will be provided
+ * to {@link GcSnapshot.Drawable#draw(Graphics2D, Paint_Delegate)}.
+ * <p>Note that the drawable may actually be executed several times if there are
+ * layers involved (see {@link #saveLayer(RectF, Paint_Delegate, int)}.
+ */
+ private static void draw(long nCanvas, GcSnapshot.Drawable drawable) {
+ // get the delegate from the native int.
+ BaseCanvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ canvasDelegate.mSnapshot.draw(drawable);
+ }
+
+ private static void drawText(long nativeCanvas, final char[] text, final int index,
+ final int count, final float startX, final float startY, final int bidiFlags,
+ long paint) {
+
+ draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
+ (graphics, paintDelegate) -> {
+ // WARNING: the logic in this method is similar to Paint_Delegate.measureText.
+ // Any change to this method should be reflected in Paint.measureText
+
+ // Paint.TextAlign indicates how the text is positioned relative to X.
+ // LEFT is the default and there's nothing to do.
+ float x = startX;
+ int limit = index + count;
+ if (paintDelegate.getTextAlign() != Paint.Align.LEFT.nativeInt) {
+ RectF bounds =
+ paintDelegate.measureText(text, index, count, null, 0, bidiFlags);
+ float m = bounds.right - bounds.left;
+ if (paintDelegate.getTextAlign() == Paint.Align.CENTER.nativeInt) {
+ x -= m / 2;
+ } else if (paintDelegate.getTextAlign() == Paint.Align.RIGHT.nativeInt) {
+ x -= m;
+ }
+ }
+
+ new BidiRenderer(graphics, paintDelegate, text).setRenderLocation(x,
+ startY).renderText(index, limit, bidiFlags, null, 0, true);
+ });
+ }
+
+ private static void drawBitmap(long nativeCanvas, Bitmap_Delegate bitmap,
+ long nativePaintOrZero, final int sleft, final int stop, final int sright,
+ final int sbottom, final int dleft, final int dtop, final int dright,
+ final int dbottom) {
+ // get the delegate from the native int.
+ BaseCanvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ // get the paint, which could be null if the int is 0
+ Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(nativePaintOrZero);
+
+ final BufferedImage image = getImageToDraw(bitmap, paintDelegate, sBoolOut);
+
+ draw(nativeCanvas, nativePaintOrZero, true /*compositeOnly*/, sBoolOut[0],
+ (graphics, paint) -> {
+ if (paint != null && paint.isFilterBitmap()) {
+ graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+ RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ }
+
+ //FIXME add support for canvas, screen and bitmap densities.
+ graphics.drawImage(image, dleft, dtop, dright, dbottom, sleft, stop, sright,
+ sbottom, null);
+ });
+ }
+
+ /**
+ * Returns a BufferedImage ready for drawing, based on the bitmap and paint delegate.
+ * The image returns, through a 1-size boolean array, whether the drawing code should
+ * use a SRC composite no matter what the paint says.
+ *
+ * @param bitmap the bitmap
+ * @param paint the paint that will be used to draw
+ * @param forceSrcMode whether the composite will have to be SRC
+ * @return the image to draw
+ */
+ private static BufferedImage getImageToDraw(Bitmap_Delegate bitmap, Paint_Delegate paint,
+ boolean[] forceSrcMode) {
+ BufferedImage image = bitmap.getImage();
+ forceSrcMode[0] = false;
+
+ // if the bitmap config is alpha_8, then we erase all color value from it
+ // before drawing it or apply the texture from the shader if present.
+ if (bitmap.getConfig() == Bitmap.Config.ALPHA_8) {
+ Shader_Delegate shader = paint.getShader();
+ java.awt.Paint javaPaint = null;
+ if (shader instanceof BitmapShader_Delegate) {
+ javaPaint = shader.getJavaPaint();
+ }
+
+ fixAlpha8Bitmap(image, javaPaint);
+ } else if (!bitmap.hasAlpha()) {
+ // hasAlpha is merely a rendering hint. There can in fact be alpha values
+ // in the bitmap but it should be ignored at drawing time.
+ // There is two ways to do this:
+ // - override the composite to be SRC. This can only be used if the composite
+ // was going to be SRC or SRC_OVER in the first place
+ // - Create a different bitmap to draw in which all the alpha channel values is set
+ // to 0xFF.
+ if (paint != null) {
+ PorterDuff.Mode mode = PorterDuff.intToMode(paint.getPorterDuffMode());
+
+ forceSrcMode[0] = mode == PorterDuff.Mode.SRC_OVER || mode == PorterDuff.Mode.SRC;
+ }
+
+ // if we can't force SRC mode, then create a temp bitmap of TYPE_RGB
+ if (!forceSrcMode[0]) {
+ image = Bitmap_Delegate.createCopy(image, BufferedImage.TYPE_INT_RGB, 0xFF);
+ }
+ }
+
+ return image;
+ }
+
+ /**
+ * This method will apply the correct color to the passed "only alpha" image. Colors on the
+ * passed image will be destroyed.
+ * If the passed javaPaint is null, the color will be set to 0. If a paint is passed, it will
+ * be used to obtain the color that will be applied.
+ * <p/>
+ * This will destroy the passed image color channel.
+ */
+ private static void fixAlpha8Bitmap(final BufferedImage image,
+ @Nullable java.awt.Paint javaPaint) {
+ int w = image.getWidth();
+ int h = image.getHeight();
+
+ DataBuffer texture = null;
+ if (javaPaint != null) {
+ PaintContext context = javaPaint.createContext(ColorModel.getRGBdefault(), null, null,
+ new AffineTransform(), null);
+ texture = context.getRaster(0, 0, w, h).getDataBuffer();
+ }
+
+ int[] argb = new int[w * h];
+ image.getRGB(0, 0, image.getWidth(), image.getHeight(), argb, 0, image.getWidth());
+
+ final int length = argb.length;
+ for (int i = 0; i < length; i++) {
+ argb[i] &= 0xFF000000;
+ if (texture != null) {
+ argb[i] |= texture.getElem(i) & 0x00FFFFFF;
+ }
+ }
+
+ image.setRGB(0, 0, w, h, argb, 0, w);
+ }
+
+ protected int save(int saveFlags) {
+ // get the current save count
+ int count = mSnapshot.size();
+
+ mSnapshot = mSnapshot.save(saveFlags);
+
+ // return the old save count
+ return count;
+ }
+
+ protected int saveLayerAlpha(RectF rect, int alpha, int saveFlags) {
+ Paint_Delegate paint = new Paint_Delegate();
+ paint.setAlpha(alpha);
+ return saveLayer(rect, paint, saveFlags);
+ }
+
+ protected int saveLayer(RectF rect, Paint_Delegate paint, int saveFlags) {
+ // get the current save count
+ int count = mSnapshot.size();
+
+ mSnapshot = mSnapshot.saveLayer(rect, paint, saveFlags);
+
+ // return the old save count
+ return count;
+ }
+
+ /**
+ * Restores the {@link GcSnapshot} to <var>saveCount</var>
+ * @param saveCount the saveCount
+ */
+ protected void restoreTo(int saveCount) {
+ mSnapshot = mSnapshot.restoreTo(saveCount);
+ }
+
+ /**
+ * Restores the top {@link GcSnapshot}
+ */
+ protected void restore() {
+ mSnapshot = mSnapshot.restore();
+ }
+
+ protected boolean clipRect(float left, float top, float right, float bottom, int regionOp) {
+ return mSnapshot.clipRect(left, top, right, bottom, regionOp);
+ }
+}
diff --git a/android/graphics/BidiRenderer.java b/android/graphics/BidiRenderer.java
new file mode 100644
index 0000000..9664a58
--- /dev/null
+++ b/android/graphics/BidiRenderer.java
@@ -0,0 +1,320 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Paint_Delegate.FontInfo;
+import android.icu.lang.UScriptRun;
+import android.icu.text.Bidi;
+import android.icu.text.BidiRun;
+
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Toolkit;
+import java.awt.font.FontRenderContext;
+import java.awt.font.GlyphVector;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Render the text by breaking it into various scripts and using the right font for each script.
+ * Can be used to measure the text without actually drawing it.
+ */
+@SuppressWarnings("deprecation")
+public class BidiRenderer {
+ private static String JAVA_VENDOR = System.getProperty("java.vendor");
+
+ private static class ScriptRun {
+ private final int start;
+ private final int limit;
+ private final Font font;
+
+ private ScriptRun(int start, int limit, @NonNull Font font) {
+ this.start = start;
+ this.limit = limit;
+ this.font = font;
+ }
+ }
+
+ private final Graphics2D mGraphics;
+ private final Paint_Delegate mPaint;
+ private char[] mText;
+ // Bounds of the text drawn so far.
+ private RectF mBounds;
+ private float mBaseline;
+ private final Bidi mBidi = new Bidi();
+
+
+ /**
+ * @param graphics May be null.
+ * @param paint The Paint to use to get the fonts. Should not be null.
+ * @param text Unidirectional text. Should not be null.
+ */
+ public BidiRenderer(Graphics2D graphics, Paint_Delegate paint, char[] text) {
+ assert (paint != null);
+ mGraphics = graphics;
+ mPaint = paint;
+ mText = text;
+ mBounds = new RectF();
+ }
+
+ /**
+ *
+ * @param x The x-coordinate of the left edge of where the text should be drawn on the given
+ * graphics.
+ * @param y The y-coordinate at which to draw the text on the given mGraphics.
+ *
+ */
+ public BidiRenderer setRenderLocation(float x, float y) {
+ mBounds.set(x, y, x, y);
+ mBaseline = y;
+ return this;
+ }
+
+ /**
+ * Perform Bidi Analysis on the text and then render it.
+ * <p/>
+ * To skip the analysis and render unidirectional text, see {@link
+ * #renderText(int, int, boolean, float[], int, boolean)}
+ */
+ public RectF renderText(int start, int limit, int bidiFlags, float[] advances,
+ int advancesIndex, boolean draw) {
+ mBidi.setPara(Arrays.copyOfRange(mText, start, limit), (byte)getIcuFlags(bidiFlags), null);
+ mText = mBidi.getText();
+ for (int i = 0; i < mBidi.countRuns(); i++) {
+ BidiRun visualRun = mBidi.getVisualRun(i);
+ boolean isRtl = visualRun.getDirection() == Bidi.RTL;
+ renderText(visualRun.getStart(), visualRun.getLimit(), isRtl, advances,
+ advancesIndex, draw);
+ }
+ return mBounds;
+ }
+
+ /**
+ * Render unidirectional text.
+ * <p/>
+ * This method can also be used to measure the width of the text without actually drawing it.
+ * <p/>
+ * @param start index of the first character
+ * @param limit index of the first character that should not be rendered.
+ * @param isRtl is the text right-to-left
+ * @param advances If not null, then advances for each character to be rendered are returned
+ * here.
+ * @param advancesIndex index into advances from where the advances need to be filled.
+ * @param draw If true and {@code graphics} is not null, draw the rendered text on the graphics
+ * at the given co-ordinates
+ * @return A rectangle specifying the bounds of the text drawn.
+ */
+ public RectF renderText(int start, int limit, boolean isRtl, float[] advances,
+ int advancesIndex, boolean draw) {
+ // We break the text into scripts and then select font based on it and then render each of
+ // the script runs.
+ for (ScriptRun run : getScriptRuns(mText, start, limit, mPaint.getFonts())) {
+ int flag = Font.LAYOUT_NO_LIMIT_CONTEXT | Font.LAYOUT_NO_START_CONTEXT;
+ flag |= isRtl ? Font.LAYOUT_RIGHT_TO_LEFT : Font.LAYOUT_LEFT_TO_RIGHT;
+ renderScript(run.start, run.limit, run.font, flag, advances, advancesIndex, draw);
+ advancesIndex += run.limit - run.start;
+ }
+ return mBounds;
+ }
+
+ /**
+ * Render a script run to the right of the bounds passed. Use the preferred font to render as
+ * much as possible. This also implements a fallback mechanism to render characters that cannot
+ * be drawn using the preferred font.
+ */
+ private void renderScript(int start, int limit, Font preferredFont, int flag,
+ float[] advances, int advancesIndex, boolean draw) {
+ if (mPaint.getFonts().size() == 0 || preferredFont == null) {
+ return;
+ }
+
+ while (start < limit) {
+ boolean foundFont = false;
+ int canDisplayUpTo = preferredFont.canDisplayUpTo(mText, start, limit);
+ if (canDisplayUpTo == -1) {
+ // We can draw all characters in the text.
+ render(start, limit, preferredFont, flag, advances, advancesIndex, draw);
+ return;
+ }
+ if (canDisplayUpTo > start) {
+ // We can draw something.
+ render(start, canDisplayUpTo, preferredFont, flag, advances, advancesIndex, draw);
+ advancesIndex += canDisplayUpTo - start;
+ start = canDisplayUpTo;
+ }
+
+ // The current character cannot be drawn with the preferred font. Cycle through all the
+ // fonts to check which one can draw it.
+ int charCount = Character.isHighSurrogate(mText[start]) ? 2 : 1;
+ List<FontInfo> fontInfos = mPaint.getFonts();
+ //noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
+ for (int i = 0; i < fontInfos.size(); i++) {
+ Font font = fontInfos.get(i).mFont;
+ if (font == null) {
+ logFontWarning();
+ continue;
+ }
+ canDisplayUpTo = font.canDisplayUpTo(mText, start, start + charCount);
+ if (canDisplayUpTo == -1) {
+ render(start, start+charCount, font, flag, advances, advancesIndex, draw);
+ start += charCount;
+ advancesIndex += charCount;
+ foundFont = true;
+ break;
+ }
+ }
+ if (!foundFont) {
+ // No font can display this char. Use the preferred font. The char will most
+ // probably appear as a box or a blank space. We could, probably, use some
+ // heuristics and break the character into the base character and diacritics and
+ // then draw it, but it's probably not worth the effort.
+ render(start, start + charCount, preferredFont, flag, advances, advancesIndex,
+ draw);
+ start += charCount;
+ advancesIndex += charCount;
+ }
+ }
+ }
+
+ private static void logFontWarning() {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN,
+ "Some fonts could not be loaded. The rendering may not be perfect. " +
+ "Try running the IDE with JRE 7.", null, null);
+ }
+
+ /**
+ * Renders the text to the right of the bounds with the given font.
+ * @param font The font to render the text with.
+ */
+ private void render(int start, int limit, Font font, int flag, float[] advances,
+ int advancesIndex, boolean draw) {
+
+ FontRenderContext frc;
+ if (mGraphics != null) {
+ frc = mGraphics.getFontRenderContext();
+ } else {
+ frc = Toolkit.getDefaultToolkit().getFontMetrics(font).getFontRenderContext();
+
+ // Metrics obtained this way don't have anti-aliasing set. So,
+ // we create a new FontRenderContext with anti-aliasing set.
+ AffineTransform transform = font.getTransform();
+ if (mPaint.isAntiAliased() &&
+ // Workaround for http://b.android.com/211659
+ (transform.getScaleX() <= 9.9 ||
+ !"JetBrains s.r.o".equals(JAVA_VENDOR))) {
+ frc = new FontRenderContext(transform, true, frc.usesFractionalMetrics());
+ }
+ }
+ GlyphVector gv = font.layoutGlyphVector(frc, mText, start, limit, flag);
+ int ng = gv.getNumGlyphs();
+ int[] ci = gv.getGlyphCharIndices(0, ng, null);
+ if (advances != null) {
+ for (int i = 0; i < ng; i++) {
+ int adv_idx = advancesIndex + ci[i];
+ advances[adv_idx] += gv.getGlyphMetrics(i).getAdvanceX();
+ }
+ }
+ if (draw && mGraphics != null) {
+ mGraphics.drawGlyphVector(gv, mBounds.right, mBaseline);
+ }
+
+ // Update the bounds.
+ Rectangle2D awtBounds = gv.getLogicalBounds();
+ // If the width of the bounds is zero, no text had been drawn earlier. Hence, use the
+ // coordinates from the bounds as an offset.
+ if (Math.abs(mBounds.right - mBounds.left) == 0) {
+ mBounds = awtRectToAndroidRect(awtBounds, mBounds.right, mBaseline, mBounds);
+ } else {
+ mBounds.union(awtRectToAndroidRect(awtBounds, mBounds.right, mBaseline, null));
+ }
+ }
+
+ // --- Static helper methods ---
+
+ private static RectF awtRectToAndroidRect(Rectangle2D awtRec, float offsetX, float offsetY,
+ @Nullable RectF destination) {
+ float left = (float) awtRec.getX();
+ float top = (float) awtRec.getY();
+ float right = (float) (left + awtRec.getWidth());
+ float bottom = (float) (top + awtRec.getHeight());
+ if (destination != null) {
+ destination.set(left, top, right, bottom);
+ } else {
+ destination = new RectF(left, top, right, bottom);
+ }
+ destination.offset(offsetX, offsetY);
+ return destination;
+ }
+
+ private static List<ScriptRun> getScriptRuns(char[] text, int start, int limit, List<FontInfo> fonts) {
+ LinkedList<ScriptRun> scriptRuns = new LinkedList<>();
+
+ int count = limit - start;
+ UScriptRun uScriptRun = new UScriptRun(text, start, count);
+ while (uScriptRun.next()) {
+ int scriptStart = uScriptRun.getScriptStart();
+ int scriptLimit = uScriptRun.getScriptLimit();
+ ScriptRun run = new ScriptRun(
+ scriptStart, scriptLimit,
+ getScriptFont(text, scriptStart, scriptLimit, fonts));
+ scriptRuns.add(run);
+ }
+ return scriptRuns;
+ }
+
+ // TODO: Replace this method with one which returns the font based on the scriptCode.
+ @NonNull
+ private static Font getScriptFont(char[] text, int start, int limit, List<FontInfo> fonts) {
+ for (FontInfo fontInfo : fonts) {
+ if (fontInfo.mFont == null) {
+ logFontWarning();
+ continue;
+ }
+ if (fontInfo.mFont.canDisplayUpTo(text, start, limit) == -1) {
+ return fontInfo.mFont;
+ }
+ }
+
+ return fonts.get(0).mFont;
+ }
+
+ private static int getIcuFlags(int bidiFlag) {
+ switch (bidiFlag) {
+ case Paint.BIDI_LTR:
+ case Paint.BIDI_FORCE_LTR:
+ return Bidi.DIRECTION_LEFT_TO_RIGHT;
+ case Paint.BIDI_RTL:
+ case Paint.BIDI_FORCE_RTL:
+ return Bidi.DIRECTION_RIGHT_TO_LEFT;
+ case Paint.BIDI_DEFAULT_LTR:
+ return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
+ case Paint.BIDI_DEFAULT_RTL:
+ return Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT;
+ default:
+ assert false;
+ return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
+ }
+ }
+}
diff --git a/android/graphics/Bitmap.java b/android/graphics/Bitmap.java
new file mode 100644
index 0000000..57c7549
--- /dev/null
+++ b/android/graphics/Bitmap.java
@@ -0,0 +1,1993 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.annotation.CheckResult;
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.content.res.ResourcesImpl;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.StrictMode;
+import android.os.Trace;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import libcore.util.NativeAllocationRegistry;
+
+import java.io.OutputStream;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+
+public final class Bitmap implements Parcelable {
+ private static final String TAG = "Bitmap";
+
+ /**
+ * Indicates that the bitmap was created for an unknown pixel density.
+ *
+ * @see Bitmap#getDensity()
+ * @see Bitmap#setDensity(int)
+ */
+ public static final int DENSITY_NONE = 0;
+
+ // Estimated size of the Bitmap native allocation, not including
+ // pixel data.
+ private static final long NATIVE_ALLOCATION_SIZE = 32;
+
+ // Convenience for JNI access
+ private final long mNativePtr;
+
+ private final boolean mIsMutable;
+
+ /**
+ * Represents whether the Bitmap's content is requested to be pre-multiplied.
+ * Note that isPremultiplied() does not directly return this value, because
+ * isPremultiplied() may never return true for a 565 Bitmap or a bitmap
+ * without alpha.
+ *
+ * setPremultiplied() does directly set the value so that setConfig() and
+ * setPremultiplied() aren't order dependent, despite being setters.
+ *
+ * The native bitmap's premultiplication state is kept up to date by
+ * pushing down this preference for every config change.
+ */
+ private boolean mRequestPremultiplied;
+
+ private byte[] mNinePatchChunk; // may be null
+ private NinePatch.InsetStruct mNinePatchInsets; // may be null
+ private int mWidth;
+ private int mHeight;
+ private boolean mRecycled;
+
+ private ColorSpace mColorSpace;
+
+ /** @hide */
+ public int mDensity = getDefaultDensity();
+
+ private static volatile int sDefaultDensity = -1;
+
+ /** @hide Used only when ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD is true. */
+ public static volatile int sPreloadTracingNumInstantiatedBitmaps;
+
+ /** @hide Used only when ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD is true. */
+ public static volatile long sPreloadTracingTotalBitmapsSize;
+
+ /**
+ * For backwards compatibility, allows the app layer to change the default
+ * density when running old apps.
+ * @hide
+ */
+ public static void setDefaultDensity(int density) {
+ sDefaultDensity = density;
+ }
+
+ @SuppressWarnings("deprecation")
+ static int getDefaultDensity() {
+ if (sDefaultDensity >= 0) {
+ return sDefaultDensity;
+ }
+ sDefaultDensity = DisplayMetrics.DENSITY_DEVICE;
+ return sDefaultDensity;
+ }
+
+ /**
+ * Private constructor that must received an already allocated native bitmap
+ * int (pointer).
+ */
+ // called from JNI
+ Bitmap(long nativeBitmap, int width, int height, int density,
+ boolean isMutable, boolean requestPremultiplied,
+ byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
+ if (nativeBitmap == 0) {
+ throw new RuntimeException("internal error: native bitmap is 0");
+ }
+
+ mWidth = width;
+ mHeight = height;
+ mIsMutable = isMutable;
+ mRequestPremultiplied = requestPremultiplied;
+
+ mNinePatchChunk = ninePatchChunk;
+ mNinePatchInsets = ninePatchInsets;
+ if (density >= 0) {
+ mDensity = density;
+ }
+
+ mNativePtr = nativeBitmap;
+ long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
+ NativeAllocationRegistry registry = new NativeAllocationRegistry(
+ Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
+ registry.registerNativeAllocation(this, nativeBitmap);
+
+ if (ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD) {
+ sPreloadTracingNumInstantiatedBitmaps++;
+ sPreloadTracingTotalBitmapsSize += nativeSize;
+ }
+ }
+
+ /**
+ * Return the pointer to the native object.
+ * @hide
+ */
+ public long getNativeInstance() {
+ return mNativePtr;
+ }
+
+ /**
+ * Native bitmap has been reconfigured, so set premult and cached
+ * width/height values
+ */
+ @SuppressWarnings("unused") // called from JNI
+ void reinit(int width, int height, boolean requestPremultiplied) {
+ mWidth = width;
+ mHeight = height;
+ mRequestPremultiplied = requestPremultiplied;
+ mColorSpace = null;
+ }
+
+ /**
+ * <p>Returns the density for this bitmap.</p>
+ *
+ * <p>The default density is the same density as the current display,
+ * unless the current application does not support different screen
+ * densities in which case it is
+ * {@link android.util.DisplayMetrics#DENSITY_DEFAULT}. Note that
+ * compatibility mode is determined by the application that was initially
+ * loaded into a process -- applications that share the same process should
+ * all have the same compatibility, or ensure they explicitly set the
+ * density of their bitmaps appropriately.</p>
+ *
+ * @return A scaling factor of the default density or {@link #DENSITY_NONE}
+ * if the scaling factor is unknown.
+ *
+ * @see #setDensity(int)
+ * @see android.util.DisplayMetrics#DENSITY_DEFAULT
+ * @see android.util.DisplayMetrics#densityDpi
+ * @see #DENSITY_NONE
+ */
+ public int getDensity() {
+ if (mRecycled) {
+ Log.w(TAG, "Called getDensity() on a recycle()'d bitmap! This is undefined behavior!");
+ }
+ return mDensity;
+ }
+
+ /**
+ * <p>Specifies the density for this bitmap. When the bitmap is
+ * drawn to a Canvas that also has a density, it will be scaled
+ * appropriately.</p>
+ *
+ * @param density The density scaling factor to use with this bitmap or
+ * {@link #DENSITY_NONE} if the density is unknown.
+ *
+ * @see #getDensity()
+ * @see android.util.DisplayMetrics#DENSITY_DEFAULT
+ * @see android.util.DisplayMetrics#densityDpi
+ * @see #DENSITY_NONE
+ */
+ public void setDensity(int density) {
+ mDensity = density;
+ }
+
+ /**
+ * <p>Modifies the bitmap to have a specified width, height, and {@link
+ * Config}, without affecting the underlying allocation backing the bitmap.
+ * Bitmap pixel data is not re-initialized for the new configuration.</p>
+ *
+ * <p>This method can be used to avoid allocating a new bitmap, instead
+ * reusing an existing bitmap's allocation for a new configuration of equal
+ * or lesser size. If the Bitmap's allocation isn't large enough to support
+ * the new configuration, an IllegalArgumentException will be thrown and the
+ * bitmap will not be modified.</p>
+ *
+ * <p>The result of {@link #getByteCount()} will reflect the new configuration,
+ * while {@link #getAllocationByteCount()} will reflect that of the initial
+ * configuration.</p>
+ *
+ * <p>Note: This may change this result of hasAlpha(). When converting to 565,
+ * the new bitmap will always be considered opaque. When converting from 565,
+ * the new bitmap will be considered non-opaque, and will respect the value
+ * set by setPremultiplied().</p>
+ *
+ * <p>WARNING: This method should NOT be called on a bitmap currently in use
+ * by the view system, Canvas, or the AndroidBitmap NDK API. It does not
+ * make guarantees about how the underlying pixel buffer is remapped to the
+ * new config, just that the allocation is reused. Additionally, the view
+ * system does not account for bitmap properties being modifying during use,
+ * e.g. while attached to drawables.</p>
+ *
+ * <p>In order to safely ensure that a Bitmap is no longer in use by the
+ * View system it is necessary to wait for a draw pass to occur after
+ * invalidate()'ing any view that had previously drawn the Bitmap in the last
+ * draw pass due to hardware acceleration's caching of draw commands. As
+ * an example, here is how this can be done for an ImageView:
+ * <pre class="prettyprint">
+ * ImageView myImageView = ...;
+ * final Bitmap myBitmap = ...;
+ * myImageView.setImageDrawable(null);
+ * myImageView.post(new Runnable() {
+ * public void run() {
+ * // myBitmap is now no longer in use by the ImageView
+ * // and can be safely reconfigured.
+ * myBitmap.reconfigure(...);
+ * }
+ * });
+ * </pre></p>
+ *
+ * @see #setWidth(int)
+ * @see #setHeight(int)
+ * @see #setConfig(Config)
+ */
+ public void reconfigure(int width, int height, Config config) {
+ checkRecycled("Can't call reconfigure() on a recycled bitmap");
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("width and height must be > 0");
+ }
+ if (!isMutable()) {
+ throw new IllegalStateException("only mutable bitmaps may be reconfigured");
+ }
+
+ nativeReconfigure(mNativePtr, width, height, config.nativeInt, mRequestPremultiplied);
+ mWidth = width;
+ mHeight = height;
+ mColorSpace = null;
+ }
+
+ /**
+ * <p>Convenience method for calling {@link #reconfigure(int, int, Config)}
+ * with the current height and config.</p>
+ *
+ * <p>WARNING: this method should not be used on bitmaps currently used by
+ * the view system, see {@link #reconfigure(int, int, Config)} for more
+ * details.</p>
+ *
+ * @see #reconfigure(int, int, Config)
+ * @see #setHeight(int)
+ * @see #setConfig(Config)
+ */
+ public void setWidth(int width) {
+ reconfigure(width, getHeight(), getConfig());
+ }
+
+ /**
+ * <p>Convenience method for calling {@link #reconfigure(int, int, Config)}
+ * with the current width and config.</p>
+ *
+ * <p>WARNING: this method should not be used on bitmaps currently used by
+ * the view system, see {@link #reconfigure(int, int, Config)} for more
+ * details.</p>
+ *
+ * @see #reconfigure(int, int, Config)
+ * @see #setWidth(int)
+ * @see #setConfig(Config)
+ */
+ public void setHeight(int height) {
+ reconfigure(getWidth(), height, getConfig());
+ }
+
+ /**
+ * <p>Convenience method for calling {@link #reconfigure(int, int, Config)}
+ * with the current height and width.</p>
+ *
+ * <p>WARNING: this method should not be used on bitmaps currently used by
+ * the view system, see {@link #reconfigure(int, int, Config)} for more
+ * details.</p>
+ *
+ * @see #reconfigure(int, int, Config)
+ * @see #setWidth(int)
+ * @see #setHeight(int)
+ */
+ public void setConfig(Config config) {
+ reconfigure(getWidth(), getHeight(), config);
+ }
+
+ /**
+ * Sets the nine patch chunk.
+ *
+ * @param chunk The definition of the nine patch
+ *
+ * @hide
+ */
+ public void setNinePatchChunk(byte[] chunk) {
+ mNinePatchChunk = chunk;
+ }
+
+ /**
+ * Free the native object associated with this bitmap, and clear the
+ * reference to the pixel data. This will not free the pixel data synchronously;
+ * it simply allows it to be garbage collected if there are no other references.
+ * The bitmap is marked as "dead", meaning it will throw an exception if
+ * getPixels() or setPixels() is called, and will draw nothing. This operation
+ * cannot be reversed, so it should only be called if you are sure there are no
+ * further uses for the bitmap. This is an advanced call, and normally need
+ * not be called, since the normal GC process will free up this memory when
+ * there are no more references to this bitmap.
+ */
+ public void recycle() {
+ if (!mRecycled && mNativePtr != 0) {
+ if (nativeRecycle(mNativePtr)) {
+ // return value indicates whether native pixel object was actually recycled.
+ // false indicates that it is still in use at the native level and these
+ // objects should not be collected now. They will be collected later when the
+ // Bitmap itself is collected.
+ mNinePatchChunk = null;
+ }
+ mRecycled = true;
+ }
+ }
+
+ /**
+ * Returns true if this bitmap has been recycled. If so, then it is an error
+ * to try to access its pixels, and the bitmap will not draw.
+ *
+ * @return true if the bitmap has been recycled
+ */
+ public final boolean isRecycled() {
+ return mRecycled;
+ }
+
+ /**
+ * Returns the generation ID of this bitmap. The generation ID changes
+ * whenever the bitmap is modified. This can be used as an efficient way to
+ * check if a bitmap has changed.
+ *
+ * @return The current generation ID for this bitmap.
+ */
+ public int getGenerationId() {
+ if (mRecycled) {
+ Log.w(TAG, "Called getGenerationId() on a recycle()'d bitmap! This is undefined behavior!");
+ }
+ return nativeGenerationId(mNativePtr);
+ }
+
+ /**
+ * This is called by methods that want to throw an exception if the bitmap
+ * has already been recycled.
+ */
+ private void checkRecycled(String errorMessage) {
+ if (mRecycled) {
+ throw new IllegalStateException(errorMessage);
+ }
+ }
+
+ /**
+ * This is called by methods that want to throw an exception if the bitmap
+ * is {@link Config#HARDWARE}.
+ */
+ private void checkHardware(String errorMessage) {
+ if (getConfig() == Config.HARDWARE) {
+ throw new IllegalStateException(errorMessage);
+ }
+ }
+
+ /**
+ * Common code for checking that x and y are >= 0
+ *
+ * @param x x coordinate to ensure is >= 0
+ * @param y y coordinate to ensure is >= 0
+ */
+ private static void checkXYSign(int x, int y) {
+ if (x < 0) {
+ throw new IllegalArgumentException("x must be >= 0");
+ }
+ if (y < 0) {
+ throw new IllegalArgumentException("y must be >= 0");
+ }
+ }
+
+ /**
+ * Common code for checking that width and height are > 0
+ *
+ * @param width width to ensure is > 0
+ * @param height height to ensure is > 0
+ */
+ private static void checkWidthHeight(int width, int height) {
+ if (width <= 0) {
+ throw new IllegalArgumentException("width must be > 0");
+ }
+ if (height <= 0) {
+ throw new IllegalArgumentException("height must be > 0");
+ }
+ }
+
+ /**
+ * Possible bitmap configurations. A bitmap configuration describes
+ * how pixels are stored. This affects the quality (color depth) as
+ * well as the ability to display transparent/translucent colors.
+ */
+ public enum Config {
+ // these native values must match up with the enum in SkBitmap.h
+
+ /**
+ * Each pixel is stored as a single translucency (alpha) channel.
+ * This is very useful to efficiently store masks for instance.
+ * No color information is stored.
+ * With this configuration, each pixel requires 1 byte of memory.
+ */
+ ALPHA_8 (1),
+
+ /**
+ * Each pixel is stored on 2 bytes and only the RGB channels are
+ * encoded: red is stored with 5 bits of precision (32 possible
+ * values), green is stored with 6 bits of precision (64 possible
+ * values) and blue is stored with 5 bits of precision.
+ *
+ * This configuration can produce slight visual artifacts depending
+ * on the configuration of the source. For instance, without
+ * dithering, the result might show a greenish tint. To get better
+ * results dithering should be applied.
+ *
+ * This configuration may be useful when using opaque bitmaps
+ * that do not require high color fidelity.
+ */
+ RGB_565 (3),
+
+ /**
+ * Each pixel is stored on 2 bytes. The three RGB color channels
+ * and the alpha channel (translucency) are stored with a 4 bits
+ * precision (16 possible values.)
+ *
+ * This configuration is mostly useful if the application needs
+ * to store translucency information but also needs to save
+ * memory.
+ *
+ * It is recommended to use {@link #ARGB_8888} instead of this
+ * configuration.
+ *
+ * Note: as of {@link android.os.Build.VERSION_CODES#KITKAT},
+ * any bitmap created with this configuration will be created
+ * using {@link #ARGB_8888} instead.
+ *
+ * @deprecated Because of the poor quality of this configuration,
+ * it is advised to use {@link #ARGB_8888} instead.
+ */
+ @Deprecated
+ ARGB_4444 (4),
+
+ /**
+ * Each pixel is stored on 4 bytes. Each channel (RGB and alpha
+ * for translucency) is stored with 8 bits of precision (256
+ * possible values.)
+ *
+ * This configuration is very flexible and offers the best
+ * quality. It should be used whenever possible.
+ */
+ ARGB_8888 (5),
+
+ /**
+ * Each pixels is stored on 8 bytes. Each channel (RGB and alpha
+ * for translucency) is stored as a
+ * {@link android.util.Half half-precision floating point value}.
+ *
+ * This configuration is particularly suited for wide-gamut and
+ * HDR content.
+ */
+ RGBA_F16 (6),
+
+ /**
+ * Special configuration, when bitmap is stored only in graphic memory.
+ * Bitmaps in this configuration are always immutable.
+ *
+ * It is optimal for cases, when the only operation with the bitmap is to draw it on a
+ * screen.
+ */
+ HARDWARE (7);
+
+ final int nativeInt;
+
+ private static Config sConfigs[] = {
+ null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE
+ };
+
+ Config(int ni) {
+ this.nativeInt = ni;
+ }
+
+ static Config nativeToConfig(int ni) {
+ return sConfigs[ni];
+ }
+ }
+
+ /**
+ * <p>Copy the bitmap's pixels into the specified buffer (allocated by the
+ * caller). An exception is thrown if the buffer is not large enough to
+ * hold all of the pixels (taking into account the number of bytes per
+ * pixel) or if the Buffer subclass is not one of the support types
+ * (ByteBuffer, ShortBuffer, IntBuffer).</p>
+ * <p>The content of the bitmap is copied into the buffer as-is. This means
+ * that if this bitmap stores its pixels pre-multiplied
+ * (see {@link #isPremultiplied()}, the values in the buffer will also be
+ * pre-multiplied. The pixels remain in the color space of the bitmap.</p>
+ * <p>After this method returns, the current position of the buffer is
+ * updated: the position is incremented by the number of elements written
+ * in the buffer.</p>
+ * @throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE}
+ */
+ public void copyPixelsToBuffer(Buffer dst) {
+ checkHardware("unable to copyPixelsToBuffer, "
+ + "pixel access is not supported on Config#HARDWARE bitmaps");
+ int elements = dst.remaining();
+ int shift;
+ if (dst instanceof ByteBuffer) {
+ shift = 0;
+ } else if (dst instanceof ShortBuffer) {
+ shift = 1;
+ } else if (dst instanceof IntBuffer) {
+ shift = 2;
+ } else {
+ throw new RuntimeException("unsupported Buffer subclass");
+ }
+
+ long bufferSize = (long)elements << shift;
+ long pixelSize = getByteCount();
+
+ if (bufferSize < pixelSize) {
+ throw new RuntimeException("Buffer not large enough for pixels");
+ }
+
+ nativeCopyPixelsToBuffer(mNativePtr, dst);
+
+ // now update the buffer's position
+ int position = dst.position();
+ position += pixelSize >> shift;
+ dst.position(position);
+ }
+
+ /**
+ * <p>Copy the pixels from the buffer, beginning at the current position,
+ * overwriting the bitmap's pixels. The data in the buffer is not changed
+ * in any way (unlike setPixels(), which converts from unpremultipled 32bit
+ * to whatever the bitmap's native format is. The pixels in the source
+ * buffer are assumed to be in the bitmap's color space.</p>
+ * <p>After this method returns, the current position of the buffer is
+ * updated: the position is incremented by the number of elements read from
+ * the buffer. If you need to read the bitmap from the buffer again you must
+ * first rewind the buffer.</p>
+ * @throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE}
+ */
+ public void copyPixelsFromBuffer(Buffer src) {
+ checkRecycled("copyPixelsFromBuffer called on recycled bitmap");
+ checkHardware("unable to copyPixelsFromBuffer, Config#HARDWARE bitmaps are immutable");
+
+ int elements = src.remaining();
+ int shift;
+ if (src instanceof ByteBuffer) {
+ shift = 0;
+ } else if (src instanceof ShortBuffer) {
+ shift = 1;
+ } else if (src instanceof IntBuffer) {
+ shift = 2;
+ } else {
+ throw new RuntimeException("unsupported Buffer subclass");
+ }
+
+ long bufferBytes = (long) elements << shift;
+ long bitmapBytes = getByteCount();
+
+ if (bufferBytes < bitmapBytes) {
+ throw new RuntimeException("Buffer not large enough for pixels");
+ }
+
+ nativeCopyPixelsFromBuffer(mNativePtr, src);
+
+ // now update the buffer's position
+ int position = src.position();
+ position += bitmapBytes >> shift;
+ src.position(position);
+ }
+
+ private void noteHardwareBitmapSlowCall() {
+ if (getConfig() == Config.HARDWARE) {
+ StrictMode.noteSlowCall("Warning: attempt to read pixels from hardware "
+ + "bitmap, which is very slow operation");
+ }
+ }
+
+ /**
+ * Tries to make a new bitmap based on the dimensions of this bitmap,
+ * setting the new bitmap's config to the one specified, and then copying
+ * this bitmap's pixels into the new bitmap. If the conversion is not
+ * supported, or the allocator fails, then this returns NULL. The returned
+ * bitmap has the same density and color space as the original.
+ *
+ * @param config The desired config for the resulting bitmap
+ * @param isMutable True if the resulting bitmap should be mutable (i.e.
+ * its pixels can be modified)
+ * @return the new bitmap, or null if the copy could not be made.
+ * @throws IllegalArgumentException if config is {@link Config#HARDWARE} and isMutable is true
+ */
+ public Bitmap copy(Config config, boolean isMutable) {
+ checkRecycled("Can't copy a recycled bitmap");
+ if (config == Config.HARDWARE && isMutable) {
+ throw new IllegalArgumentException("Hardware bitmaps are always immutable");
+ }
+ noteHardwareBitmapSlowCall();
+ Bitmap b = nativeCopy(mNativePtr, config.nativeInt, isMutable);
+ if (b != null) {
+ b.setPremultiplied(mRequestPremultiplied);
+ b.mDensity = mDensity;
+ }
+ return b;
+ }
+
+ /**
+ * Creates a new immutable bitmap backed by ashmem which can efficiently
+ * be passed between processes. The bitmap is assumed to be in the sRGB
+ * color space.
+ *
+ * @hide
+ */
+ public Bitmap createAshmemBitmap() {
+ checkRecycled("Can't copy a recycled bitmap");
+ noteHardwareBitmapSlowCall();
+ Bitmap b = nativeCopyAshmem(mNativePtr);
+ if (b != null) {
+ b.setPremultiplied(mRequestPremultiplied);
+ b.mDensity = mDensity;
+ }
+ return b;
+ }
+
+ /**
+ * Creates a new immutable bitmap backed by ashmem which can efficiently
+ * be passed between processes. The bitmap is assumed to be in the sRGB
+ * color space.
+ *
+ * @hide
+ */
+ public Bitmap createAshmemBitmap(Config config) {
+ checkRecycled("Can't copy a recycled bitmap");
+ noteHardwareBitmapSlowCall();
+ Bitmap b = nativeCopyAshmemConfig(mNativePtr, config.nativeInt);
+ if (b != null) {
+ b.setPremultiplied(mRequestPremultiplied);
+ b.mDensity = mDensity;
+ }
+ return b;
+ }
+
+ /**
+ * Create hardware bitmap backed GraphicBuffer.
+ *
+ * @return Bitmap or null if this GraphicBuffer has unsupported PixelFormat.
+ * currently PIXEL_FORMAT_RGBA_8888 is the only supported format
+ * @hide
+ */
+ public static Bitmap createHardwareBitmap(@NonNull GraphicBuffer graphicBuffer) {
+ return nativeCreateHardwareBitmap(graphicBuffer);
+ }
+
+ /**
+ * Creates a new bitmap, scaled from an existing bitmap, when possible. If the
+ * specified width and height are the same as the current width and height of
+ * the source bitmap, the source bitmap is returned and no new bitmap is
+ * created.
+ *
+ * @param src The source bitmap.
+ * @param dstWidth The new bitmap's desired width.
+ * @param dstHeight The new bitmap's desired height.
+ * @param filter true if the source should be filtered.
+ * @return The new scaled bitmap or the source bitmap if no scaling is required.
+ * @throws IllegalArgumentException if width is <= 0, or height is <= 0
+ */
+ public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,
+ boolean filter) {
+ Matrix m = new Matrix();
+
+ final int width = src.getWidth();
+ final int height = src.getHeight();
+ if (width != dstWidth || height != dstHeight) {
+ final float sx = dstWidth / (float) width;
+ final float sy = dstHeight / (float) height;
+ m.setScale(sx, sy);
+ }
+ return Bitmap.createBitmap(src, 0, 0, width, height, m, filter);
+ }
+
+ /**
+ * Returns an immutable bitmap from the source bitmap. The new bitmap may
+ * be the same object as source, or a copy may have been made. It is
+ * initialized with the same density and color space as the original bitmap.
+ */
+ public static Bitmap createBitmap(@NonNull Bitmap src) {
+ return createBitmap(src, 0, 0, src.getWidth(), src.getHeight());
+ }
+
+ /**
+ * Returns an immutable bitmap from the specified subset of the source
+ * bitmap. The new bitmap may be the same object as source, or a copy may
+ * have been made. It is initialized with the same density and color space
+ * as the original bitmap.
+ *
+ * @param source The bitmap we are subsetting
+ * @param x The x coordinate of the first pixel in source
+ * @param y The y coordinate of the first pixel in source
+ * @param width The number of pixels in each row
+ * @param height The number of rows
+ * @return A copy of a subset of the source bitmap or the source bitmap itself.
+ * @throws IllegalArgumentException if the x, y, width, height values are
+ * outside of the dimensions of the source bitmap, or width is <= 0,
+ * or height is <= 0
+ */
+ public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height) {
+ return createBitmap(source, x, y, width, height, null, false);
+ }
+
+ /**
+ * Returns an immutable bitmap from subset of the source bitmap,
+ * transformed by the optional matrix. The new bitmap may be the
+ * same object as source, or a copy may have been made. It is
+ * initialized with the same density and color space as the original
+ * bitmap.
+ *
+ * If the source bitmap is immutable and the requested subset is the
+ * same as the source bitmap itself, then the source bitmap is
+ * returned and no new bitmap is created.
+ *
+ * @param source The bitmap we are subsetting
+ * @param x The x coordinate of the first pixel in source
+ * @param y The y coordinate of the first pixel in source
+ * @param width The number of pixels in each row
+ * @param height The number of rows
+ * @param m Optional matrix to be applied to the pixels
+ * @param filter true if the source should be filtered.
+ * Only applies if the matrix contains more than just
+ * translation.
+ * @return A bitmap that represents the specified subset of source
+ * @throws IllegalArgumentException if the x, y, width, height values are
+ * outside of the dimensions of the source bitmap, or width is <= 0,
+ * or height is <= 0
+ */
+ public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height,
+ @Nullable Matrix m, boolean filter) {
+
+ checkXYSign(x, y);
+ checkWidthHeight(width, height);
+ if (x + width > source.getWidth()) {
+ throw new IllegalArgumentException("x + width must be <= bitmap.width()");
+ }
+ if (y + height > source.getHeight()) {
+ throw new IllegalArgumentException("y + height must be <= bitmap.height()");
+ }
+
+ // check if we can just return our argument unchanged
+ if (!source.isMutable() && x == 0 && y == 0 && width == source.getWidth() &&
+ height == source.getHeight() && (m == null || m.isIdentity())) {
+ return source;
+ }
+
+ boolean isHardware = source.getConfig() == Config.HARDWARE;
+ if (isHardware) {
+ source.noteHardwareBitmapSlowCall();
+ source = nativeCopyPreserveInternalConfig(source.mNativePtr);
+ }
+
+ int neww = width;
+ int newh = height;
+ Bitmap bitmap;
+ Paint paint;
+
+ Rect srcR = new Rect(x, y, x + width, y + height);
+ RectF dstR = new RectF(0, 0, width, height);
+ RectF deviceR = new RectF();
+
+ Config newConfig = Config.ARGB_8888;
+ final Config config = source.getConfig();
+ // GIF files generate null configs, assume ARGB_8888
+ if (config != null) {
+ switch (config) {
+ case RGB_565:
+ newConfig = Config.RGB_565;
+ break;
+ case ALPHA_8:
+ newConfig = Config.ALPHA_8;
+ break;
+ case RGBA_F16:
+ newConfig = Config.RGBA_F16;
+ break;
+ //noinspection deprecation
+ case ARGB_4444:
+ case ARGB_8888:
+ default:
+ newConfig = Config.ARGB_8888;
+ break;
+ }
+ }
+
+ if (m == null || m.isIdentity()) {
+ bitmap = createBitmap(neww, newh, newConfig, source.hasAlpha());
+ paint = null; // not needed
+ } else {
+ final boolean transformed = !m.rectStaysRect();
+
+ m.mapRect(deviceR, dstR);
+
+ neww = Math.round(deviceR.width());
+ newh = Math.round(deviceR.height());
+
+ Config transformedConfig = newConfig;
+ if (transformed) {
+ if (transformedConfig != Config.ARGB_8888 && transformedConfig != Config.RGBA_F16) {
+ transformedConfig = Config.ARGB_8888;
+ }
+ }
+ bitmap = createBitmap(neww, newh, transformedConfig, transformed || source.hasAlpha());
+
+ paint = new Paint();
+ paint.setFilterBitmap(filter);
+ if (transformed) {
+ paint.setAntiAlias(true);
+ }
+ }
+
+ nativeCopyColorSpace(source.mNativePtr, bitmap.mNativePtr);
+
+ // The new bitmap was created from a known bitmap source so assume that
+ // they use the same density
+ bitmap.mDensity = source.mDensity;
+ bitmap.setHasAlpha(source.hasAlpha());
+ bitmap.setPremultiplied(source.mRequestPremultiplied);
+
+ Canvas canvas = new Canvas(bitmap);
+ canvas.translate(-deviceR.left, -deviceR.top);
+ canvas.concat(m);
+ canvas.drawBitmap(source, srcR, dstR, paint);
+ canvas.setBitmap(null);
+ if (isHardware) {
+ return bitmap.copy(Config.HARDWARE, false);
+ }
+ return bitmap;
+ }
+
+ /**
+ * Returns a mutable bitmap with the specified width and height. Its
+ * initial density is as per {@link #getDensity}. The newly created
+ * bitmap is in the {@link ColorSpace.Named#SRGB sRGB} color space.
+ *
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param config The bitmap config to create.
+ * @throws IllegalArgumentException if the width or height are <= 0, or if
+ * Config is Config.HARDWARE, because hardware bitmaps are always immutable
+ */
+ public static Bitmap createBitmap(int width, int height, @NonNull Config config) {
+ return createBitmap(width, height, config, true);
+ }
+
+ /**
+ * Returns a mutable bitmap with the specified width and height. Its
+ * initial density is determined from the given {@link DisplayMetrics}.
+ * The newly created bitmap is in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @param display Display metrics for the display this bitmap will be
+ * drawn on.
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param config The bitmap config to create.
+ * @throws IllegalArgumentException if the width or height are <= 0, or if
+ * Config is Config.HARDWARE, because hardware bitmaps are always immutable
+ */
+ public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width,
+ int height, @NonNull Config config) {
+ return createBitmap(display, width, height, config, true);
+ }
+
+ /**
+ * Returns a mutable bitmap with the specified width and height. Its
+ * initial density is as per {@link #getDensity}. The newly created
+ * bitmap is in the {@link ColorSpace.Named#SRGB sRGB} color space.
+ *
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param config The bitmap config to create.
+ * @param hasAlpha If the bitmap is ARGB_8888 or RGBA_16F this flag can be used to
+ * mark the bitmap as opaque. Doing so will clear the bitmap in black
+ * instead of transparent.
+ *
+ * @throws IllegalArgumentException if the width or height are <= 0, or if
+ * Config is Config.HARDWARE, because hardware bitmaps are always immutable
+ */
+ public static Bitmap createBitmap(int width, int height,
+ @NonNull Config config, boolean hasAlpha) {
+ return createBitmap(null, width, height, config, hasAlpha);
+ }
+
+ /**
+ * Returns a mutable bitmap with the specified width and height. Its
+ * initial density is as per {@link #getDensity}.
+ *
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param config The bitmap config to create.
+ * @param hasAlpha If the bitmap is ARGB_8888 or RGBA_16F this flag can be used to
+ * mark the bitmap as opaque. Doing so will clear the bitmap in black
+ * instead of transparent.
+ * @param colorSpace The color space of the bitmap. If the config is {@link Config#RGBA_F16},
+ * {@link ColorSpace.Named#EXTENDED_SRGB scRGB} is assumed, and if the
+ * config is not {@link Config#ARGB_8888}, {@link ColorSpace.Named#SRGB sRGB}
+ * is assumed.
+ *
+ * @throws IllegalArgumentException if the width or height are <= 0, if
+ * Config is Config.HARDWARE (because hardware bitmaps are always
+ * immutable), if the specified color space is not {@link ColorSpace.Model#RGB RGB},
+ * if the specified color space's transfer function is not an
+ * {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}, or if
+ * the color space is null
+ */
+ public static Bitmap createBitmap(int width, int height, @NonNull Config config,
+ boolean hasAlpha, @NonNull ColorSpace colorSpace) {
+ return createBitmap(null, width, height, config, hasAlpha, colorSpace);
+ }
+
+ /**
+ * Returns a mutable bitmap with the specified width and height. Its
+ * initial density is determined from the given {@link DisplayMetrics}.
+ * The newly created bitmap is in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @param display Display metrics for the display this bitmap will be
+ * drawn on.
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param config The bitmap config to create.
+ * @param hasAlpha If the bitmap is ARGB_8888 or RGBA_16F this flag can be used to
+ * mark the bitmap as opaque. Doing so will clear the bitmap in black
+ * instead of transparent.
+ *
+ * @throws IllegalArgumentException if the width or height are <= 0, or if
+ * Config is Config.HARDWARE, because hardware bitmaps are always immutable
+ */
+ public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
+ @NonNull Config config, boolean hasAlpha) {
+ return createBitmap(display, width, height, config, hasAlpha,
+ ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+
+ /**
+ * Returns a mutable bitmap with the specified width and height. Its
+ * initial density is determined from the given {@link DisplayMetrics}.
+ * The newly created bitmap is in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @param display Display metrics for the display this bitmap will be
+ * drawn on.
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param config The bitmap config to create.
+ * @param hasAlpha If the bitmap is ARGB_8888 or RGBA_16F this flag can be used to
+ * mark the bitmap as opaque. Doing so will clear the bitmap in black
+ * instead of transparent.
+ * @param colorSpace The color space of the bitmap. If the config is {@link Config#RGBA_F16},
+ * {@link ColorSpace.Named#EXTENDED_SRGB scRGB} is assumed, and if the
+ * config is not {@link Config#ARGB_8888}, {@link ColorSpace.Named#SRGB sRGB}
+ * is assumed.
+ *
+ * @throws IllegalArgumentException if the width or height are <= 0, if
+ * Config is Config.HARDWARE (because hardware bitmaps are always
+ * immutable), if the specified color space is not {@link ColorSpace.Model#RGB RGB},
+ * if the specified color space's transfer function is not an
+ * {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}, or if
+ * the color space is null
+ */
+ public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
+ @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("width and height must be > 0");
+ }
+ if (config == Config.HARDWARE) {
+ throw new IllegalArgumentException("can't create mutable bitmap with Config.HARDWARE");
+ }
+ if (colorSpace == null) {
+ throw new IllegalArgumentException("can't create bitmap without a color space");
+ }
+
+ Bitmap bm;
+ // nullptr color spaces have a particular meaning in native and are interpreted as sRGB
+ // (we also avoid the unnecessary extra work of the else branch)
+ if (config != Config.ARGB_8888 || colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) {
+ bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, null, null);
+ } else {
+ if (!(colorSpace instanceof ColorSpace.Rgb)) {
+ throw new IllegalArgumentException("colorSpace must be an RGB color space");
+ }
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace;
+ ColorSpace.Rgb.TransferParameters parameters = rgb.getTransferParameters();
+ if (parameters == null) {
+ throw new IllegalArgumentException("colorSpace must use an ICC "
+ + "parametric transfer function");
+ }
+
+ ColorSpace.Rgb d50 = (ColorSpace.Rgb) ColorSpace.adapt(rgb, ColorSpace.ILLUMINANT_D50);
+ bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
+ d50.getTransform(), parameters);
+ }
+
+ if (display != null) {
+ bm.mDensity = display.densityDpi;
+ }
+ bm.setHasAlpha(hasAlpha);
+ if ((config == Config.ARGB_8888 || config == Config.RGBA_F16) && !hasAlpha) {
+ nativeErase(bm.mNativePtr, 0xff000000);
+ }
+ // No need to initialize the bitmap to zeroes with other configs;
+ // it is backed by a VM byte array which is by definition preinitialized
+ // to all zeroes.
+ return bm;
+ }
+
+ /**
+ * Returns a immutable bitmap with the specified width and height, with each
+ * pixel value set to the corresponding value in the colors array. Its
+ * initial density is as per {@link #getDensity}. The newly created
+ * bitmap is in the {@link ColorSpace.Named#SRGB sRGB} color space.
+ *
+ * @param colors Array of sRGB {@link Color colors} used to initialize the pixels.
+ * @param offset Number of values to skip before the first color in the
+ * array of colors.
+ * @param stride Number of colors in the array between rows (must be >=
+ * width or <= -width).
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param config The bitmap config to create. If the config does not
+ * support per-pixel alpha (e.g. RGB_565), then the alpha
+ * bytes in the colors[] will be ignored (assumed to be FF)
+ * @throws IllegalArgumentException if the width or height are <= 0, or if
+ * the color array's length is less than the number of pixels.
+ */
+ public static Bitmap createBitmap(@NonNull @ColorInt int[] colors, int offset, int stride,
+ int width, int height, @NonNull Config config) {
+ return createBitmap(null, colors, offset, stride, width, height, config);
+ }
+
+ /**
+ * Returns a immutable bitmap with the specified width and height, with each
+ * pixel value set to the corresponding value in the colors array. Its
+ * initial density is determined from the given {@link DisplayMetrics}.
+ * The newly created bitmap is in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @param display Display metrics for the display this bitmap will be
+ * drawn on.
+ * @param colors Array of sRGB {@link Color colors} used to initialize the pixels.
+ * @param offset Number of values to skip before the first color in the
+ * array of colors.
+ * @param stride Number of colors in the array between rows (must be >=
+ * width or <= -width).
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param config The bitmap config to create. If the config does not
+ * support per-pixel alpha (e.g. RGB_565), then the alpha
+ * bytes in the colors[] will be ignored (assumed to be FF)
+ * @throws IllegalArgumentException if the width or height are <= 0, or if
+ * the color array's length is less than the number of pixels.
+ */
+ public static Bitmap createBitmap(@NonNull DisplayMetrics display,
+ @NonNull @ColorInt int[] colors, int offset, int stride,
+ int width, int height, @NonNull Config config) {
+
+ checkWidthHeight(width, height);
+ if (Math.abs(stride) < width) {
+ throw new IllegalArgumentException("abs(stride) must be >= width");
+ }
+ int lastScanline = offset + (height - 1) * stride;
+ int length = colors.length;
+ if (offset < 0 || (offset + width > length) || lastScanline < 0 ||
+ (lastScanline + width > length)) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("width and height must be > 0");
+ }
+ Bitmap bm = nativeCreate(colors, offset, stride, width, height,
+ config.nativeInt, false, null, null);
+ if (display != null) {
+ bm.mDensity = display.densityDpi;
+ }
+ return bm;
+ }
+
+ /**
+ * Returns a immutable bitmap with the specified width and height, with each
+ * pixel value set to the corresponding value in the colors array. Its
+ * initial density is as per {@link #getDensity}. The newly created
+ * bitmap is in the {@link ColorSpace.Named#SRGB sRGB} color space.
+ *
+ * @param colors Array of sRGB {@link Color colors} used to initialize the pixels.
+ * This array must be at least as large as width * height.
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param config The bitmap config to create. If the config does not
+ * support per-pixel alpha (e.g. RGB_565), then the alpha
+ * bytes in the colors[] will be ignored (assumed to be FF)
+ * @throws IllegalArgumentException if the width or height are <= 0, or if
+ * the color array's length is less than the number of pixels.
+ */
+ public static Bitmap createBitmap(@NonNull @ColorInt int[] colors,
+ int width, int height, Config config) {
+ return createBitmap(null, colors, 0, width, width, height, config);
+ }
+
+ /**
+ * Returns a immutable bitmap with the specified width and height, with each
+ * pixel value set to the corresponding value in the colors array. Its
+ * initial density is determined from the given {@link DisplayMetrics}.
+ * The newly created bitmap is in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @param display Display metrics for the display this bitmap will be
+ * drawn on.
+ * @param colors Array of sRGB {@link Color colors} used to initialize the pixels.
+ * This array must be at least as large as width * height.
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param config The bitmap config to create. If the config does not
+ * support per-pixel alpha (e.g. RGB_565), then the alpha
+ * bytes in the colors[] will be ignored (assumed to be FF)
+ * @throws IllegalArgumentException if the width or height are <= 0, or if
+ * the color array's length is less than the number of pixels.
+ */
+ public static Bitmap createBitmap(@Nullable DisplayMetrics display,
+ @NonNull @ColorInt int colors[], int width, int height, @NonNull Config config) {
+ return createBitmap(display, colors, 0, width, width, height, config);
+ }
+
+ /**
+ * Returns an optional array of private data, used by the UI system for
+ * some bitmaps. Not intended to be called by applications.
+ */
+ public byte[] getNinePatchChunk() {
+ return mNinePatchChunk;
+ }
+
+ /**
+ * Populates a rectangle with the bitmap's optical insets.
+ *
+ * @param outInsets Rect to populate with optical insets
+ * @hide
+ */
+ public void getOpticalInsets(@NonNull Rect outInsets) {
+ if (mNinePatchInsets == null) {
+ outInsets.setEmpty();
+ } else {
+ outInsets.set(mNinePatchInsets.opticalRect);
+ }
+ }
+
+ /** @hide */
+ public NinePatch.InsetStruct getNinePatchInsets() {
+ return mNinePatchInsets;
+ }
+
+ /**
+ * Specifies the known formats a bitmap can be compressed into
+ */
+ public enum CompressFormat {
+ JPEG (0),
+ PNG (1),
+ WEBP (2);
+
+ CompressFormat(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ /**
+ * Number of bytes of temp storage we use for communicating between the
+ * native compressor and the java OutputStream.
+ */
+ private final static int WORKING_COMPRESS_STORAGE = 4096;
+
+ /**
+ * Write a compressed version of the bitmap to the specified outputstream.
+ * If this returns true, the bitmap can be reconstructed by passing a
+ * corresponding inputstream to BitmapFactory.decodeStream(). Note: not
+ * all Formats support all bitmap configs directly, so it is possible that
+ * the returned bitmap from BitmapFactory could be in a different bitdepth,
+ * and/or may have lost per-pixel alpha (e.g. JPEG only supports opaque
+ * pixels).
+ *
+ * @param format The format of the compressed image
+ * @param quality Hint to the compressor, 0-100. 0 meaning compress for
+ * small size, 100 meaning compress for max quality. Some
+ * formats, like PNG which is lossless, will ignore the
+ * quality setting
+ * @param stream The outputstream to write the compressed data.
+ * @return true if successfully compressed to the specified stream.
+ */
+ public boolean compress(CompressFormat format, int quality, OutputStream stream) {
+ checkRecycled("Can't compress a recycled bitmap");
+ // do explicit check before calling the native method
+ if (stream == null) {
+ throw new NullPointerException();
+ }
+ if (quality < 0 || quality > 100) {
+ throw new IllegalArgumentException("quality must be 0..100");
+ }
+ StrictMode.noteSlowCall("Compression of a bitmap is slow");
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
+ boolean result = nativeCompress(mNativePtr, format.nativeInt,
+ quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ return result;
+ }
+
+ /**
+ * Returns true if the bitmap is marked as mutable (i.e. can be drawn into)
+ */
+ public final boolean isMutable() {
+ return mIsMutable;
+ }
+
+ /**
+ * <p>Indicates whether pixels stored in this bitmaps are stored pre-multiplied.
+ * When a pixel is pre-multiplied, the RGB components have been multiplied by
+ * the alpha component. For instance, if the original color is a 50%
+ * translucent red <code>(128, 255, 0, 0)</code>, the pre-multiplied form is
+ * <code>(128, 128, 0, 0)</code>.</p>
+ *
+ * <p>This method always returns false if {@link #getConfig()} is
+ * {@link Bitmap.Config#RGB_565}.</p>
+ *
+ * <p>The return value is undefined if {@link #getConfig()} is
+ * {@link Bitmap.Config#ALPHA_8}.</p>
+ *
+ * <p>This method only returns true if {@link #hasAlpha()} returns true.
+ * A bitmap with no alpha channel can be used both as a pre-multiplied and
+ * as a non pre-multiplied bitmap.</p>
+ *
+ * <p>Only pre-multiplied bitmaps may be drawn by the view system or
+ * {@link Canvas}. If a non-pre-multiplied bitmap with an alpha channel is
+ * drawn to a Canvas, a RuntimeException will be thrown.</p>
+ *
+ * @return true if the underlying pixels have been pre-multiplied, false
+ * otherwise
+ *
+ * @see Bitmap#setPremultiplied(boolean)
+ * @see BitmapFactory.Options#inPremultiplied
+ */
+ public final boolean isPremultiplied() {
+ if (mRecycled) {
+ Log.w(TAG, "Called isPremultiplied() on a recycle()'d bitmap! This is undefined behavior!");
+ }
+ return nativeIsPremultiplied(mNativePtr);
+ }
+
+ /**
+ * Sets whether the bitmap should treat its data as pre-multiplied.
+ *
+ * <p>Bitmaps are always treated as pre-multiplied by the view system and
+ * {@link Canvas} for performance reasons. Storing un-pre-multiplied data in
+ * a Bitmap (through {@link #setPixel}, {@link #setPixels}, or {@link
+ * BitmapFactory.Options#inPremultiplied BitmapFactory.Options.inPremultiplied})
+ * can lead to incorrect blending if drawn by the framework.</p>
+ *
+ * <p>This method will not affect the behavior of a bitmap without an alpha
+ * channel, or if {@link #hasAlpha()} returns false.</p>
+ *
+ * <p>Calling {@link #createBitmap} or {@link #createScaledBitmap} with a source
+ * Bitmap whose colors are not pre-multiplied may result in a RuntimeException,
+ * since those functions require drawing the source, which is not supported for
+ * un-pre-multiplied Bitmaps.</p>
+ *
+ * @see Bitmap#isPremultiplied()
+ * @see BitmapFactory.Options#inPremultiplied
+ */
+ public final void setPremultiplied(boolean premultiplied) {
+ checkRecycled("setPremultiplied called on a recycled bitmap");
+ mRequestPremultiplied = premultiplied;
+ nativeSetPremultiplied(mNativePtr, premultiplied);
+ }
+
+ /** Returns the bitmap's width */
+ public final int getWidth() {
+ if (mRecycled) {
+ Log.w(TAG, "Called getWidth() on a recycle()'d bitmap! This is undefined behavior!");
+ }
+ return mWidth;
+ }
+
+ /** Returns the bitmap's height */
+ public final int getHeight() {
+ if (mRecycled) {
+ Log.w(TAG, "Called getHeight() on a recycle()'d bitmap! This is undefined behavior!");
+ }
+ return mHeight;
+ }
+
+ /**
+ * Convenience for calling {@link #getScaledWidth(int)} with the target
+ * density of the given {@link Canvas}.
+ */
+ public int getScaledWidth(Canvas canvas) {
+ return scaleFromDensity(getWidth(), mDensity, canvas.mDensity);
+ }
+
+ /**
+ * Convenience for calling {@link #getScaledHeight(int)} with the target
+ * density of the given {@link Canvas}.
+ */
+ public int getScaledHeight(Canvas canvas) {
+ return scaleFromDensity(getHeight(), mDensity, canvas.mDensity);
+ }
+
+ /**
+ * Convenience for calling {@link #getScaledWidth(int)} with the target
+ * density of the given {@link DisplayMetrics}.
+ */
+ public int getScaledWidth(DisplayMetrics metrics) {
+ return scaleFromDensity(getWidth(), mDensity, metrics.densityDpi);
+ }
+
+ /**
+ * Convenience for calling {@link #getScaledHeight(int)} with the target
+ * density of the given {@link DisplayMetrics}.
+ */
+ public int getScaledHeight(DisplayMetrics metrics) {
+ return scaleFromDensity(getHeight(), mDensity, metrics.densityDpi);
+ }
+
+ /**
+ * Convenience method that returns the width of this bitmap divided
+ * by the density scale factor.
+ *
+ * @param targetDensity The density of the target canvas of the bitmap.
+ * @return The scaled width of this bitmap, according to the density scale factor.
+ */
+ public int getScaledWidth(int targetDensity) {
+ return scaleFromDensity(getWidth(), mDensity, targetDensity);
+ }
+
+ /**
+ * Convenience method that returns the height of this bitmap divided
+ * by the density scale factor.
+ *
+ * @param targetDensity The density of the target canvas of the bitmap.
+ * @return The scaled height of this bitmap, according to the density scale factor.
+ */
+ public int getScaledHeight(int targetDensity) {
+ return scaleFromDensity(getHeight(), mDensity, targetDensity);
+ }
+
+ /**
+ * @hide
+ */
+ static public int scaleFromDensity(int size, int sdensity, int tdensity) {
+ if (sdensity == DENSITY_NONE || tdensity == DENSITY_NONE || sdensity == tdensity) {
+ return size;
+ }
+
+ // Scale by tdensity / sdensity, rounding up.
+ return ((size * tdensity) + (sdensity >> 1)) / sdensity;
+ }
+
+ /**
+ * Return the number of bytes between rows in the bitmap's pixels. Note that
+ * this refers to the pixels as stored natively by the bitmap. If you call
+ * getPixels() or setPixels(), then the pixels are uniformly treated as
+ * 32bit values, packed according to the Color class.
+ *
+ * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, this method
+ * should not be used to calculate the memory usage of the bitmap. Instead,
+ * see {@link #getAllocationByteCount()}.
+ *
+ * @return number of bytes between rows of the native bitmap pixels.
+ */
+ public final int getRowBytes() {
+ if (mRecycled) {
+ Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
+ }
+ return nativeRowBytes(mNativePtr);
+ }
+
+ /**
+ * Returns the minimum number of bytes that can be used to store this bitmap's pixels.
+ *
+ * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can
+ * no longer be used to determine memory usage of a bitmap. See {@link
+ * #getAllocationByteCount()}.</p>
+ */
+ public final int getByteCount() {
+ if (mRecycled) {
+ Log.w(TAG, "Called getByteCount() on a recycle()'d bitmap! "
+ + "This is undefined behavior!");
+ return 0;
+ }
+ // int result permits bitmaps up to 46,340 x 46,340
+ return getRowBytes() * getHeight();
+ }
+
+ /**
+ * Returns the size of the allocated memory used to store this bitmap's pixels.
+ *
+ * <p>This can be larger than the result of {@link #getByteCount()} if a bitmap is reused to
+ * decode other bitmaps of smaller size, or by manual reconfiguration. See {@link
+ * #reconfigure(int, int, Config)}, {@link #setWidth(int)}, {@link #setHeight(int)}, {@link
+ * #setConfig(Bitmap.Config)}, and {@link BitmapFactory.Options#inBitmap
+ * BitmapFactory.Options.inBitmap}. If a bitmap is not modified in this way, this value will be
+ * the same as that returned by {@link #getByteCount()}.</p>
+ *
+ * <p>This value will not change over the lifetime of a Bitmap.</p>
+ *
+ * @see #reconfigure(int, int, Config)
+ */
+ public final int getAllocationByteCount() {
+ if (mRecycled) {
+ Log.w(TAG, "Called getAllocationByteCount() on a recycle()'d bitmap! "
+ + "This is undefined behavior!");
+ return 0;
+ }
+ return nativeGetAllocationByteCount(mNativePtr);
+ }
+
+ /**
+ * If the bitmap's internal config is in one of the public formats, return
+ * that config, otherwise return null.
+ */
+ public final Config getConfig() {
+ if (mRecycled) {
+ Log.w(TAG, "Called getConfig() on a recycle()'d bitmap! This is undefined behavior!");
+ }
+ return Config.nativeToConfig(nativeConfig(mNativePtr));
+ }
+
+ /** Returns true if the bitmap's config supports per-pixel alpha, and
+ * if the pixels may contain non-opaque alpha values. For some configs,
+ * this is always false (e.g. RGB_565), since they do not support per-pixel
+ * alpha. However, for configs that do, the bitmap may be flagged to be
+ * known that all of its pixels are opaque. In this case hasAlpha() will
+ * also return false. If a config such as ARGB_8888 is not so flagged,
+ * it will return true by default.
+ */
+ public final boolean hasAlpha() {
+ if (mRecycled) {
+ Log.w(TAG, "Called hasAlpha() on a recycle()'d bitmap! This is undefined behavior!");
+ }
+ return nativeHasAlpha(mNativePtr);
+ }
+
+ /**
+ * Tell the bitmap if all of the pixels are known to be opaque (false)
+ * or if some of the pixels may contain non-opaque alpha values (true).
+ * Note, for some configs (e.g. RGB_565) this call is ignored, since it
+ * does not support per-pixel alpha values.
+ *
+ * This is meant as a drawing hint, as in some cases a bitmap that is known
+ * to be opaque can take a faster drawing case than one that may have
+ * non-opaque per-pixel alpha values.
+ */
+ public void setHasAlpha(boolean hasAlpha) {
+ checkRecycled("setHasAlpha called on a recycled bitmap");
+ nativeSetHasAlpha(mNativePtr, hasAlpha, mRequestPremultiplied);
+ }
+
+ /**
+ * Indicates whether the renderer responsible for drawing this
+ * bitmap should attempt to use mipmaps when this bitmap is drawn
+ * scaled down.
+ *
+ * If you know that you are going to draw this bitmap at less than
+ * 50% of its original size, you may be able to obtain a higher
+ * quality
+ *
+ * This property is only a suggestion that can be ignored by the
+ * renderer. It is not guaranteed to have any effect.
+ *
+ * @return true if the renderer should attempt to use mipmaps,
+ * false otherwise
+ *
+ * @see #setHasMipMap(boolean)
+ */
+ public final boolean hasMipMap() {
+ if (mRecycled) {
+ Log.w(TAG, "Called hasMipMap() on a recycle()'d bitmap! This is undefined behavior!");
+ }
+ return nativeHasMipMap(mNativePtr);
+ }
+
+ /**
+ * Set a hint for the renderer responsible for drawing this bitmap
+ * indicating that it should attempt to use mipmaps when this bitmap
+ * is drawn scaled down.
+ *
+ * If you know that you are going to draw this bitmap at less than
+ * 50% of its original size, you may be able to obtain a higher
+ * quality by turning this property on.
+ *
+ * Note that if the renderer respects this hint it might have to
+ * allocate extra memory to hold the mipmap levels for this bitmap.
+ *
+ * This property is only a suggestion that can be ignored by the
+ * renderer. It is not guaranteed to have any effect.
+ *
+ * @param hasMipMap indicates whether the renderer should attempt
+ * to use mipmaps
+ *
+ * @see #hasMipMap()
+ */
+ public final void setHasMipMap(boolean hasMipMap) {
+ checkRecycled("setHasMipMap called on a recycled bitmap");
+ nativeSetHasMipMap(mNativePtr, hasMipMap);
+ }
+
+ /**
+ * Returns the color space associated with this bitmap. If the color
+ * space is unknown, this method returns null.
+ */
+ @Nullable
+ public final ColorSpace getColorSpace() {
+ // A reconfigure can change the configuration and rgba16f is
+ // always linear scRGB at this time
+ if (getConfig() == Config.RGBA_F16) {
+ // Reset the color space for potential future reconfigurations
+ mColorSpace = null;
+ return ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB);
+ }
+
+ // Cache the color space retrieval since it can be fairly expensive
+ if (mColorSpace == null) {
+ if (nativeIsSRGB(mNativePtr)) {
+ mColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+ } else {
+ float[] xyz = new float[9];
+ float[] params = new float[7];
+
+ boolean hasColorSpace = nativeGetColorSpace(mNativePtr, xyz, params);
+ if (hasColorSpace) {
+ ColorSpace.Rgb.TransferParameters parameters =
+ new ColorSpace.Rgb.TransferParameters(
+ params[0], params[1], params[2],
+ params[3], params[4], params[5], params[6]);
+ ColorSpace cs = ColorSpace.match(xyz, parameters);
+ if (cs != null) {
+ mColorSpace = cs;
+ } else {
+ mColorSpace = new ColorSpace.Rgb("Unknown", xyz, parameters);
+ }
+ }
+ }
+ }
+
+ return mColorSpace;
+ }
+
+ /**
+ * Fills the bitmap's pixels with the specified {@link Color}.
+ *
+ * @throws IllegalStateException if the bitmap is not mutable.
+ */
+ public void eraseColor(@ColorInt int c) {
+ checkRecycled("Can't erase a recycled bitmap");
+ if (!isMutable()) {
+ throw new IllegalStateException("cannot erase immutable bitmaps");
+ }
+ nativeErase(mNativePtr, c);
+ }
+
+ /**
+ * Returns the {@link Color} at the specified location. Throws an exception
+ * if x or y are out of bounds (negative or >= to the width or height
+ * respectively). The returned color is a non-premultiplied ARGB value in
+ * the {@link ColorSpace.Named#SRGB sRGB} color space.
+ *
+ * @param x The x coordinate (0...width-1) of the pixel to return
+ * @param y The y coordinate (0...height-1) of the pixel to return
+ * @return The argb {@link Color} at the specified coordinate
+ * @throws IllegalArgumentException if x, y exceed the bitmap's bounds
+ * @throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE}
+ */
+ @ColorInt
+ public int getPixel(int x, int y) {
+ checkRecycled("Can't call getPixel() on a recycled bitmap");
+ checkHardware("unable to getPixel(), "
+ + "pixel access is not supported on Config#HARDWARE bitmaps");
+ checkPixelAccess(x, y);
+ return nativeGetPixel(mNativePtr, x, y);
+ }
+
+ /**
+ * Returns in pixels[] a copy of the data in the bitmap. Each value is
+ * a packed int representing a {@link Color}. The stride parameter allows
+ * the caller to allow for gaps in the returned pixels array between
+ * rows. For normal packed results, just pass width for the stride value.
+ * The returned colors are non-premultiplied ARGB values in the
+ * {@link ColorSpace.Named#SRGB sRGB} color space.
+ *
+ * @param pixels The array to receive the bitmap's colors
+ * @param offset The first index to write into pixels[]
+ * @param stride The number of entries in pixels[] to skip between
+ * rows (must be >= bitmap's width). Can be negative.
+ * @param x The x coordinate of the first pixel to read from
+ * the bitmap
+ * @param y The y coordinate of the first pixel to read from
+ * the bitmap
+ * @param width The number of pixels to read from each row
+ * @param height The number of rows to read
+ *
+ * @throws IllegalArgumentException if x, y, width, height exceed the
+ * bounds of the bitmap, or if abs(stride) < width.
+ * @throws ArrayIndexOutOfBoundsException if the pixels array is too small
+ * to receive the specified number of pixels.
+ * @throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE}
+ */
+ public void getPixels(@ColorInt int[] pixels, int offset, int stride,
+ int x, int y, int width, int height) {
+ checkRecycled("Can't call getPixels() on a recycled bitmap");
+ checkHardware("unable to getPixels(), "
+ + "pixel access is not supported on Config#HARDWARE bitmaps");
+ if (width == 0 || height == 0) {
+ return; // nothing to do
+ }
+ checkPixelsAccess(x, y, width, height, offset, stride, pixels);
+ nativeGetPixels(mNativePtr, pixels, offset, stride,
+ x, y, width, height);
+ }
+
+ /**
+ * Shared code to check for illegal arguments passed to getPixel()
+ * or setPixel()
+ *
+ * @param x x coordinate of the pixel
+ * @param y y coordinate of the pixel
+ */
+ private void checkPixelAccess(int x, int y) {
+ checkXYSign(x, y);
+ if (x >= getWidth()) {
+ throw new IllegalArgumentException("x must be < bitmap.width()");
+ }
+ if (y >= getHeight()) {
+ throw new IllegalArgumentException("y must be < bitmap.height()");
+ }
+ }
+
+ /**
+ * Shared code to check for illegal arguments passed to getPixels()
+ * or setPixels()
+ *
+ * @param x left edge of the area of pixels to access
+ * @param y top edge of the area of pixels to access
+ * @param width width of the area of pixels to access
+ * @param height height of the area of pixels to access
+ * @param offset offset into pixels[] array
+ * @param stride number of elements in pixels[] between each logical row
+ * @param pixels array to hold the area of pixels being accessed
+ */
+ private void checkPixelsAccess(int x, int y, int width, int height,
+ int offset, int stride, int pixels[]) {
+ checkXYSign(x, y);
+ if (width < 0) {
+ throw new IllegalArgumentException("width must be >= 0");
+ }
+ if (height < 0) {
+ throw new IllegalArgumentException("height must be >= 0");
+ }
+ if (x + width > getWidth()) {
+ throw new IllegalArgumentException(
+ "x + width must be <= bitmap.width()");
+ }
+ if (y + height > getHeight()) {
+ throw new IllegalArgumentException(
+ "y + height must be <= bitmap.height()");
+ }
+ if (Math.abs(stride) < width) {
+ throw new IllegalArgumentException("abs(stride) must be >= width");
+ }
+ int lastScanline = offset + (height - 1) * stride;
+ int length = pixels.length;
+ if (offset < 0 || (offset + width > length)
+ || lastScanline < 0
+ || (lastScanline + width > length)) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ }
+
+ /**
+ * <p>Write the specified {@link Color} into the bitmap (assuming it is
+ * mutable) at the x,y coordinate. The color must be a
+ * non-premultiplied ARGB value in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.</p>
+ *
+ * @param x The x coordinate of the pixel to replace (0...width-1)
+ * @param y The y coordinate of the pixel to replace (0...height-1)
+ * @param color The ARGB color to write into the bitmap
+ *
+ * @throws IllegalStateException if the bitmap is not mutable
+ * @throws IllegalArgumentException if x, y are outside of the bitmap's
+ * bounds.
+ */
+ public void setPixel(int x, int y, @ColorInt int color) {
+ checkRecycled("Can't call setPixel() on a recycled bitmap");
+ if (!isMutable()) {
+ throw new IllegalStateException();
+ }
+ checkPixelAccess(x, y);
+ nativeSetPixel(mNativePtr, x, y, color);
+ }
+
+ /**
+ * <p>Replace pixels in the bitmap with the colors in the array. Each element
+ * in the array is a packed int representing a non-premultiplied ARGB
+ * {@link Color} in the {@link ColorSpace.Named#SRGB sRGB} color space.</p>
+ *
+ * @param pixels The colors to write to the bitmap
+ * @param offset The index of the first color to read from pixels[]
+ * @param stride The number of colors in pixels[] to skip between rows.
+ * Normally this value will be the same as the width of
+ * the bitmap, but it can be larger (or negative).
+ * @param x The x coordinate of the first pixel to write to in
+ * the bitmap.
+ * @param y The y coordinate of the first pixel to write to in
+ * the bitmap.
+ * @param width The number of colors to copy from pixels[] per row
+ * @param height The number of rows to write to the bitmap
+ *
+ * @throws IllegalStateException if the bitmap is not mutable
+ * @throws IllegalArgumentException if x, y, width, height are outside of
+ * the bitmap's bounds.
+ * @throws ArrayIndexOutOfBoundsException if the pixels array is too small
+ * to receive the specified number of pixels.
+ */
+ public void setPixels(@ColorInt int[] pixels, int offset, int stride,
+ int x, int y, int width, int height) {
+ checkRecycled("Can't call setPixels() on a recycled bitmap");
+ if (!isMutable()) {
+ throw new IllegalStateException();
+ }
+ if (width == 0 || height == 0) {
+ return; // nothing to do
+ }
+ checkPixelsAccess(x, y, width, height, offset, stride, pixels);
+ nativeSetPixels(mNativePtr, pixels, offset, stride,
+ x, y, width, height);
+ }
+
+ public static final Parcelable.Creator<Bitmap> CREATOR
+ = new Parcelable.Creator<Bitmap>() {
+ /**
+ * Rebuilds a bitmap previously stored with writeToParcel().
+ *
+ * @param p Parcel object to read the bitmap from
+ * @return a new bitmap created from the data in the parcel
+ */
+ public Bitmap createFromParcel(Parcel p) {
+ Bitmap bm = nativeCreateFromParcel(p);
+ if (bm == null) {
+ throw new RuntimeException("Failed to unparcel Bitmap");
+ }
+ return bm;
+ }
+ public Bitmap[] newArray(int size) {
+ return new Bitmap[size];
+ }
+ };
+
+ /**
+ * No special parcel contents.
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Write the bitmap and its pixels to the parcel. The bitmap can be
+ * rebuilt from the parcel by calling CREATOR.createFromParcel().
+ *
+ * If this bitmap is {@link Config#HARDWARE}, it may be unparceled with a different pixel
+ * format (e.g. 565, 8888), but the content will be preserved to the best quality permitted
+ * by the final pixel format
+ * @param p Parcel object to write the bitmap data into
+ */
+ public void writeToParcel(Parcel p, int flags) {
+ checkRecycled("Can't parcel a recycled bitmap");
+ noteHardwareBitmapSlowCall();
+ if (!nativeWriteToParcel(mNativePtr, mIsMutable, mDensity, p)) {
+ throw new RuntimeException("native writeToParcel failed");
+ }
+ }
+
+ /**
+ * Returns a new bitmap that captures the alpha values of the original.
+ * This may be drawn with Canvas.drawBitmap(), where the color(s) will be
+ * taken from the paint that is passed to the draw call.
+ *
+ * @return new bitmap containing the alpha channel of the original bitmap.
+ */
+ @CheckResult
+ public Bitmap extractAlpha() {
+ return extractAlpha(null, null);
+ }
+
+ /**
+ * Returns a new bitmap that captures the alpha values of the original.
+ * These values may be affected by the optional Paint parameter, which
+ * can contain its own alpha, and may also contain a MaskFilter which
+ * could change the actual dimensions of the resulting bitmap (e.g.
+ * a blur maskfilter might enlarge the resulting bitmap). If offsetXY
+ * is not null, it returns the amount to offset the returned bitmap so
+ * that it will logically align with the original. For example, if the
+ * paint contains a blur of radius 2, then offsetXY[] would contains
+ * -2, -2, so that drawing the alpha bitmap offset by (-2, -2) and then
+ * drawing the original would result in the blur visually aligning with
+ * the original.
+ *
+ * <p>The initial density of the returned bitmap is the same as the original's.
+ *
+ * @param paint Optional paint used to modify the alpha values in the
+ * resulting bitmap. Pass null for default behavior.
+ * @param offsetXY Optional array that returns the X (index 0) and Y
+ * (index 1) offset needed to position the returned bitmap
+ * so that it visually lines up with the original.
+ * @return new bitmap containing the (optionally modified by paint) alpha
+ * channel of the original bitmap. This may be drawn with
+ * Canvas.drawBitmap(), where the color(s) will be taken from the
+ * paint that is passed to the draw call.
+ */
+ @CheckResult
+ public Bitmap extractAlpha(Paint paint, int[] offsetXY) {
+ checkRecycled("Can't extractAlpha on a recycled bitmap");
+ long nativePaint = paint != null ? paint.getNativeInstance() : 0;
+ noteHardwareBitmapSlowCall();
+ Bitmap bm = nativeExtractAlpha(mNativePtr, nativePaint, offsetXY);
+ if (bm == null) {
+ throw new RuntimeException("Failed to extractAlpha on Bitmap");
+ }
+ bm.mDensity = mDensity;
+ return bm;
+ }
+
+ /**
+ * Given another bitmap, return true if it has the same dimensions, config,
+ * and pixel data as this bitmap. If any of those differ, return false.
+ * If other is null, return false.
+ */
+ public boolean sameAs(Bitmap other) {
+ checkRecycled("Can't call sameAs on a recycled bitmap!");
+ noteHardwareBitmapSlowCall();
+ if (this == other) return true;
+ if (other == null) return false;
+ other.noteHardwareBitmapSlowCall();
+ if (other.isRecycled()) {
+ throw new IllegalArgumentException("Can't compare to a recycled bitmap!");
+ }
+ return nativeSameAs(mNativePtr, other.mNativePtr);
+ }
+
+ /**
+ * Builds caches associated with the bitmap that are used for drawing it.
+ *
+ * <p>Starting in {@link android.os.Build.VERSION_CODES#N}, this call initiates an asynchronous
+ * upload to the GPU on RenderThread, if the Bitmap is not already uploaded. With Hardware
+ * Acceleration, Bitmaps must be uploaded to the GPU in order to be rendered. This is done by
+ * default the first time a Bitmap is drawn, but the process can take several milliseconds,
+ * depending on the size of the Bitmap. Each time a Bitmap is modified and drawn again, it must
+ * be re-uploaded.</p>
+ *
+ * <p>Calling this method in advance can save time in the first frame it's used. For example, it
+ * is recommended to call this on an image decoding worker thread when a decoded Bitmap is about
+ * to be displayed. It is recommended to make any pre-draw modifications to the Bitmap before
+ * calling this method, so the cached, uploaded copy may be reused without re-uploading.</p>
+ *
+ * In {@link android.os.Build.VERSION_CODES#KITKAT} and below, for purgeable bitmaps, this call
+ * would attempt to ensure that the pixels have been decoded.
+ */
+ public void prepareToDraw() {
+ checkRecycled("Can't prepareToDraw on a recycled bitmap!");
+ // Kick off an update/upload of the bitmap outside of the normal
+ // draw path.
+ nativePrepareToDraw(mNativePtr);
+ }
+
+ /**
+ *
+ * @return {@link GraphicBuffer} which is internally used by hardware bitmap
+ * @hide
+ */
+ public GraphicBuffer createGraphicBufferHandle() {
+ return nativeCreateGraphicBufferHandle(mNativePtr);
+ }
+
+ //////////// native methods
+
+ private static native Bitmap nativeCreate(int[] colors, int offset,
+ int stride, int width, int height,
+ int nativeConfig, boolean mutable,
+ @Nullable @Size(9) float[] xyzD50,
+ @Nullable ColorSpace.Rgb.TransferParameters p);
+ private static native Bitmap nativeCopy(long nativeSrcBitmap, int nativeConfig,
+ boolean isMutable);
+ private static native Bitmap nativeCopyAshmem(long nativeSrcBitmap);
+ private static native Bitmap nativeCopyAshmemConfig(long nativeSrcBitmap, int nativeConfig);
+ private static native long nativeGetNativeFinalizer();
+ private static native boolean nativeRecycle(long nativeBitmap);
+ private static native void nativeReconfigure(long nativeBitmap, int width, int height,
+ int config, boolean isPremultiplied);
+
+ private static native boolean nativeCompress(long nativeBitmap, int format,
+ int quality, OutputStream stream,
+ byte[] tempStorage);
+ private static native void nativeErase(long nativeBitmap, int color);
+ private static native int nativeRowBytes(long nativeBitmap);
+ private static native int nativeConfig(long nativeBitmap);
+
+ private static native int nativeGetPixel(long nativeBitmap, int x, int y);
+ private static native void nativeGetPixels(long nativeBitmap, int[] pixels,
+ int offset, int stride, int x, int y,
+ int width, int height);
+
+ private static native void nativeSetPixel(long nativeBitmap, int x, int y, int color);
+ private static native void nativeSetPixels(long nativeBitmap, int[] colors,
+ int offset, int stride, int x, int y,
+ int width, int height);
+ private static native void nativeCopyPixelsToBuffer(long nativeBitmap,
+ Buffer dst);
+ private static native void nativeCopyPixelsFromBuffer(long nativeBitmap, Buffer src);
+ private static native int nativeGenerationId(long nativeBitmap);
+
+ private static native Bitmap nativeCreateFromParcel(Parcel p);
+ // returns true on success
+ private static native boolean nativeWriteToParcel(long nativeBitmap,
+ boolean isMutable,
+ int density,
+ Parcel p);
+ // returns a new bitmap built from the native bitmap's alpha, and the paint
+ private static native Bitmap nativeExtractAlpha(long nativeBitmap,
+ long nativePaint,
+ int[] offsetXY);
+
+ private static native boolean nativeHasAlpha(long nativeBitmap);
+ private static native boolean nativeIsPremultiplied(long nativeBitmap);
+ private static native void nativeSetPremultiplied(long nativeBitmap,
+ boolean isPremul);
+ private static native void nativeSetHasAlpha(long nativeBitmap,
+ boolean hasAlpha,
+ boolean requestPremul);
+ private static native boolean nativeHasMipMap(long nativeBitmap);
+ private static native void nativeSetHasMipMap(long nativeBitmap, boolean hasMipMap);
+ private static native boolean nativeSameAs(long nativeBitmap0, long nativeBitmap1);
+ private static native void nativePrepareToDraw(long nativeBitmap);
+ private static native int nativeGetAllocationByteCount(long nativeBitmap);
+ private static native Bitmap nativeCopyPreserveInternalConfig(long nativeBitmap);
+ private static native Bitmap nativeCreateHardwareBitmap(GraphicBuffer buffer);
+ private static native GraphicBuffer nativeCreateGraphicBufferHandle(long nativeBitmap);
+ private static native boolean nativeGetColorSpace(long nativePtr, float[] xyz, float[] params);
+ private static native boolean nativeIsSRGB(long nativePtr);
+ private static native void nativeCopyColorSpace(long srcBitmap, long dstBitmap);
+}
diff --git a/android/graphics/BitmapFactory.java b/android/graphics/BitmapFactory.java
new file mode 100644
index 0000000..3b272c8
--- /dev/null
+++ b/android/graphics/BitmapFactory.java
@@ -0,0 +1,828 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import static android.graphics.BitmapFactory.Options.validate;
+
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.os.Trace;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Creates Bitmap objects from various sources, including files, streams,
+ * and byte-arrays.
+ */
+public class BitmapFactory {
+ private static final int DECODE_BUFFER_SIZE = 16 * 1024;
+
+ public static class Options {
+ /**
+ * Create a default Options object, which if left unchanged will give
+ * the same result from the decoder as if null were passed.
+ */
+ public Options() {
+ inScaled = true;
+ inPremultiplied = true;
+ }
+
+ /**
+ * If set, decode methods that take the Options object will attempt to
+ * reuse this bitmap when loading content. If the decode operation
+ * cannot use this bitmap, the decode method will throw an
+ * {@link java.lang.IllegalArgumentException}. The
+ * current implementation necessitates that the reused bitmap be
+ * mutable, and the resulting reused bitmap will continue to remain
+ * mutable even when decoding a resource which would normally result in
+ * an immutable bitmap.</p>
+ *
+ * <p>You should still always use the returned Bitmap of the decode
+ * method and not assume that reusing the bitmap worked, due to the
+ * constraints outlined above and failure situations that can occur.
+ * Checking whether the return value matches the value of the inBitmap
+ * set in the Options structure will indicate if the bitmap was reused,
+ * but in all cases you should use the Bitmap returned by the decoding
+ * function to ensure that you are using the bitmap that was used as the
+ * decode destination.</p>
+ *
+ * <h3>Usage with BitmapFactory</h3>
+ *
+ * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, any
+ * mutable bitmap can be reused by {@link BitmapFactory} to decode any
+ * other bitmaps as long as the resulting {@link Bitmap#getByteCount()
+ * byte count} of the decoded bitmap is less than or equal to the {@link
+ * Bitmap#getAllocationByteCount() allocated byte count} of the reused
+ * bitmap. This can be because the intrinsic size is smaller, or its
+ * size post scaling (for density / sample size) is smaller.</p>
+ *
+ * <p class="note">Prior to {@link android.os.Build.VERSION_CODES#KITKAT}
+ * additional constraints apply: The image being decoded (whether as a
+ * resource or as a stream) must be in jpeg or png format. Only equal
+ * sized bitmaps are supported, with {@link #inSampleSize} set to 1.
+ * Additionally, the {@link android.graphics.Bitmap.Config
+ * configuration} of the reused bitmap will override the setting of
+ * {@link #inPreferredConfig}, if set.</p>
+ *
+ * <h3>Usage with BitmapRegionDecoder</h3>
+ *
+ * <p>BitmapRegionDecoder will draw its requested content into the Bitmap
+ * provided, clipping if the output content size (post scaling) is larger
+ * than the provided Bitmap. The provided Bitmap's width, height, and
+ * {@link Bitmap.Config} will not be changed.
+ *
+ * <p class="note">BitmapRegionDecoder support for {@link #inBitmap} was
+ * introduced in {@link android.os.Build.VERSION_CODES#JELLY_BEAN}. All
+ * formats supported by BitmapRegionDecoder support Bitmap reuse via
+ * {@link #inBitmap}.</p>
+ *
+ * @see Bitmap#reconfigure(int,int, android.graphics.Bitmap.Config)
+ */
+ public Bitmap inBitmap;
+
+ /**
+ * If set, decode methods will always return a mutable Bitmap instead of
+ * an immutable one. This can be used for instance to programmatically apply
+ * effects to a Bitmap loaded through BitmapFactory.
+ * <p>Can not be set simultaneously with inPreferredConfig =
+ * {@link android.graphics.Bitmap.Config#HARDWARE},
+ * because hardware bitmaps are always immutable.
+ */
+ @SuppressWarnings({"UnusedDeclaration"}) // used in native code
+ public boolean inMutable;
+
+ /**
+ * If set to true, the decoder will return null (no bitmap), but
+ * the <code>out...</code> fields will still be set, allowing the caller to
+ * query the bitmap without having to allocate the memory for its pixels.
+ */
+ public boolean inJustDecodeBounds;
+
+ /**
+ * If set to a value > 1, requests the decoder to subsample the original
+ * image, returning a smaller image to save memory. The sample size is
+ * the number of pixels in either dimension that correspond to a single
+ * pixel in the decoded bitmap. For example, inSampleSize == 4 returns
+ * an image that is 1/4 the width/height of the original, and 1/16 the
+ * number of pixels. Any value <= 1 is treated the same as 1. Note: the
+ * decoder uses a final value based on powers of 2, any other value will
+ * be rounded down to the nearest power of 2.
+ */
+ public int inSampleSize;
+
+ /**
+ * If this is non-null, the decoder will try to decode into this
+ * internal configuration. If it is null, or the request cannot be met,
+ * the decoder will try to pick the best matching config based on the
+ * system's screen depth, and characteristics of the original image such
+ * as if it has per-pixel alpha (requiring a config that also does).
+ *
+ * Image are loaded with the {@link Bitmap.Config#ARGB_8888} config by
+ * default.
+ */
+ public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;
+
+ /**
+ * <p>If this is non-null, the decoder will try to decode into this
+ * color space. If it is null, or the request cannot be met,
+ * the decoder will pick either the color space embedded in the image
+ * or the color space best suited for the requested image configuration
+ * (for instance {@link ColorSpace.Named#SRGB sRGB} for
+ * the {@link Bitmap.Config#ARGB_8888} configuration).</p>
+ *
+ * <p>{@link Bitmap.Config#RGBA_F16} always uses the
+ * {@link ColorSpace.Named#LINEAR_EXTENDED_SRGB scRGB} color space).
+ * Bitmaps in other configurations without an embedded color space are
+ * assumed to be in the {@link ColorSpace.Named#SRGB sRGB} color space.</p>
+ *
+ * <p class="note">Only {@link ColorSpace.Model#RGB} color spaces are
+ * currently supported. An <code>IllegalArgumentException</code> will
+ * be thrown by the decode methods when setting a non-RGB color space
+ * such as {@link ColorSpace.Named#CIE_LAB Lab}.</p>
+ *
+ * <p class="note">The specified color space's transfer function must be
+ * an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An
+ * <code>IllegalArgumentException</code> will be thrown by the decode methods
+ * if calling {@link ColorSpace.Rgb#getTransferParameters()} on the
+ * specified color space returns null.</p>
+ *
+ * <p>After decode, the bitmap's color space is stored in
+ * {@link #outColorSpace}.</p>
+ */
+ public ColorSpace inPreferredColorSpace = null;
+
+ /**
+ * If true (which is the default), the resulting bitmap will have its
+ * color channels pre-multipled by the alpha channel.
+ *
+ * <p>This should NOT be set to false for images to be directly drawn by
+ * the view system or through a {@link Canvas}. The view system and
+ * {@link Canvas} assume all drawn images are pre-multiplied to simplify
+ * draw-time blending, and will throw a RuntimeException when
+ * un-premultiplied are drawn.</p>
+ *
+ * <p>This is likely only useful if you want to manipulate raw encoded
+ * image data, e.g. with RenderScript or custom OpenGL.</p>
+ *
+ * <p>This does not affect bitmaps without an alpha channel.</p>
+ *
+ * <p>Setting this flag to false while setting {@link #inScaled} to true
+ * may result in incorrect colors.</p>
+ *
+ * @see Bitmap#hasAlpha()
+ * @see Bitmap#isPremultiplied()
+ * @see #inScaled
+ */
+ public boolean inPremultiplied;
+
+ /**
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this is
+ * ignored.
+ *
+ * In {@link android.os.Build.VERSION_CODES#M} and below, if dither is
+ * true, the decoder will attempt to dither the decoded image.
+ */
+ public boolean inDither;
+
+ /**
+ * The pixel density to use for the bitmap. This will always result
+ * in the returned bitmap having a density set for it (see
+ * {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}). In addition,
+ * if {@link #inScaled} is set (which it is by default} and this
+ * density does not match {@link #inTargetDensity}, then the bitmap
+ * will be scaled to the target density before being returned.
+ *
+ * <p>If this is 0,
+ * {@link BitmapFactory#decodeResource(Resources, int)},
+ * {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)},
+ * and {@link BitmapFactory#decodeResourceStream}
+ * will fill in the density associated with the resource. The other
+ * functions will leave it as-is and no density will be applied.
+ *
+ * @see #inTargetDensity
+ * @see #inScreenDensity
+ * @see #inScaled
+ * @see Bitmap#setDensity(int)
+ * @see android.util.DisplayMetrics#densityDpi
+ */
+ public int inDensity;
+
+ /**
+ * The pixel density of the destination this bitmap will be drawn to.
+ * This is used in conjunction with {@link #inDensity} and
+ * {@link #inScaled} to determine if and how to scale the bitmap before
+ * returning it.
+ *
+ * <p>If this is 0,
+ * {@link BitmapFactory#decodeResource(Resources, int)},
+ * {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)},
+ * and {@link BitmapFactory#decodeResourceStream}
+ * will fill in the density associated the Resources object's
+ * DisplayMetrics. The other
+ * functions will leave it as-is and no scaling for density will be
+ * performed.
+ *
+ * @see #inDensity
+ * @see #inScreenDensity
+ * @see #inScaled
+ * @see android.util.DisplayMetrics#densityDpi
+ */
+ public int inTargetDensity;
+
+ /**
+ * The pixel density of the actual screen that is being used. This is
+ * purely for applications running in density compatibility code, where
+ * {@link #inTargetDensity} is actually the density the application
+ * sees rather than the real screen density.
+ *
+ * <p>By setting this, you
+ * allow the loading code to avoid scaling a bitmap that is currently
+ * in the screen density up/down to the compatibility density. Instead,
+ * if {@link #inDensity} is the same as {@link #inScreenDensity}, the
+ * bitmap will be left as-is. Anything using the resulting bitmap
+ * must also used {@link Bitmap#getScaledWidth(int)
+ * Bitmap.getScaledWidth} and {@link Bitmap#getScaledHeight
+ * Bitmap.getScaledHeight} to account for any different between the
+ * bitmap's density and the target's density.
+ *
+ * <p>This is never set automatically for the caller by
+ * {@link BitmapFactory} itself. It must be explicitly set, since the
+ * caller must deal with the resulting bitmap in a density-aware way.
+ *
+ * @see #inDensity
+ * @see #inTargetDensity
+ * @see #inScaled
+ * @see android.util.DisplayMetrics#densityDpi
+ */
+ public int inScreenDensity;
+
+ /**
+ * When this flag is set, if {@link #inDensity} and
+ * {@link #inTargetDensity} are not 0, the
+ * bitmap will be scaled to match {@link #inTargetDensity} when loaded,
+ * rather than relying on the graphics system scaling it each time it
+ * is drawn to a Canvas.
+ *
+ * <p>BitmapRegionDecoder ignores this flag, and will not scale output
+ * based on density. (though {@link #inSampleSize} is supported)</p>
+ *
+ * <p>This flag is turned on by default and should be turned off if you need
+ * a non-scaled version of the bitmap. Nine-patch bitmaps ignore this
+ * flag and are always scaled.
+ *
+ * <p>If {@link #inPremultiplied} is set to false, and the image has alpha,
+ * setting this flag to true may result in incorrect colors.
+ */
+ public boolean inScaled;
+
+ /**
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
+ * ignored.
+ *
+ * In {@link android.os.Build.VERSION_CODES#KITKAT} and below, if this
+ * is set to true, then the resulting bitmap will allocate its
+ * pixels such that they can be purged if the system needs to reclaim
+ * memory. In that instance, when the pixels need to be accessed again
+ * (e.g. the bitmap is drawn, getPixels() is called), they will be
+ * automatically re-decoded.
+ *
+ * <p>For the re-decode to happen, the bitmap must have access to the
+ * encoded data, either by sharing a reference to the input
+ * or by making a copy of it. This distinction is controlled by
+ * inInputShareable. If this is true, then the bitmap may keep a shallow
+ * reference to the input. If this is false, then the bitmap will
+ * explicitly make a copy of the input data, and keep that. Even if
+ * sharing is allowed, the implementation may still decide to make a
+ * deep copy of the input data.</p>
+ *
+ * <p>While inPurgeable can help avoid big Dalvik heap allocations (from
+ * API level 11 onward), it sacrifices performance predictability since any
+ * image that the view system tries to draw may incur a decode delay which
+ * can lead to dropped frames. Therefore, most apps should avoid using
+ * inPurgeable to allow for a fast and fluid UI. To minimize Dalvik heap
+ * allocations use the {@link #inBitmap} flag instead.</p>
+ *
+ * <p class="note"><strong>Note:</strong> This flag is ignored when used
+ * with {@link #decodeResource(Resources, int,
+ * android.graphics.BitmapFactory.Options)} or {@link #decodeFile(String,
+ * android.graphics.BitmapFactory.Options)}.</p>
+ */
+ @Deprecated
+ public boolean inPurgeable;
+
+ /**
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
+ * ignored.
+ *
+ * In {@link android.os.Build.VERSION_CODES#KITKAT} and below, this
+ * field works in conjuction with inPurgeable. If inPurgeable is false,
+ * then this field is ignored. If inPurgeable is true, then this field
+ * determines whether the bitmap can share a reference to the input
+ * data (inputstream, array, etc.) or if it must make a deep copy.
+ */
+ @Deprecated
+ public boolean inInputShareable;
+
+ /**
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this is
+ * ignored. The output will always be high quality.
+ *
+ * In {@link android.os.Build.VERSION_CODES#M} and below, if
+ * inPreferQualityOverSpeed is set to true, the decoder will try to
+ * decode the reconstructed image to a higher quality even at the
+ * expense of the decoding speed. Currently the field only affects JPEG
+ * decode, in the case of which a more accurate, but slightly slower,
+ * IDCT method will be used instead.
+ */
+ public boolean inPreferQualityOverSpeed;
+
+ /**
+ * The resulting width of the bitmap. If {@link #inJustDecodeBounds} is
+ * set to false, this will be width of the output bitmap after any
+ * scaling is applied. If true, it will be the width of the input image
+ * without any accounting for scaling.
+ *
+ * <p>outWidth will be set to -1 if there is an error trying to decode.</p>
+ */
+ public int outWidth;
+
+ /**
+ * The resulting height of the bitmap. If {@link #inJustDecodeBounds} is
+ * set to false, this will be height of the output bitmap after any
+ * scaling is applied. If true, it will be the height of the input image
+ * without any accounting for scaling.
+ *
+ * <p>outHeight will be set to -1 if there is an error trying to decode.</p>
+ */
+ public int outHeight;
+
+ /**
+ * If known, this string is set to the mimetype of the decoded image.
+ * If not known, or there is an error, it is set to null.
+ */
+ public String outMimeType;
+
+ /**
+ * If known, the config the decoded bitmap will have.
+ * If not known, or there is an error, it is set to null.
+ */
+ public Bitmap.Config outConfig;
+
+ /**
+ * If known, the color space the decoded bitmap will have. Note that the
+ * output color space is not guaranteed to be the color space the bitmap
+ * is encoded with. If not known (when the config is
+ * {@link Bitmap.Config#ALPHA_8} for instance), or there is an error,
+ * it is set to null.
+ */
+ public ColorSpace outColorSpace;
+
+ /**
+ * Temp storage to use for decoding. Suggest 16K or so.
+ */
+ public byte[] inTempStorage;
+
+ /**
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#N}, see
+ * comments on {@link #requestCancelDecode()}.
+ *
+ * Flag to indicate that cancel has been called on this object. This
+ * is useful if there's an intermediary that wants to first decode the
+ * bounds and then decode the image. In that case the intermediary
+ * can check, inbetween the bounds decode and the image decode, to see
+ * if the operation is canceled.
+ */
+ public boolean mCancel;
+
+ /**
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this
+ * will not affect the decode, though it will still set mCancel.
+ *
+ * In {@link android.os.Build.VERSION_CODES#M} and below, if this can
+ * be called from another thread while this options object is inside
+ * a decode... call. Calling this will notify the decoder that it
+ * should cancel its operation. This is not guaranteed to cancel the
+ * decode, but if it does, the decoder... operation will return null,
+ * or if inJustDecodeBounds is true, will set outWidth/outHeight
+ * to -1
+ */
+ public void requestCancelDecode() {
+ mCancel = true;
+ }
+
+ static void validate(Options opts) {
+ if (opts == null) return;
+
+ if (opts.inMutable && opts.inPreferredConfig == Bitmap.Config.HARDWARE) {
+ throw new IllegalArgumentException("Bitmaps with Config.HARWARE are always immutable");
+ }
+
+ if (opts.inPreferredColorSpace != null) {
+ if (!(opts.inPreferredColorSpace instanceof ColorSpace.Rgb)) {
+ throw new IllegalArgumentException("The destination color space must use the " +
+ "RGB color model");
+ }
+ if (((ColorSpace.Rgb) opts.inPreferredColorSpace).getTransferParameters() == null) {
+ throw new IllegalArgumentException("The destination color space must use an " +
+ "ICC parametric transfer function");
+ }
+ }
+ }
+ }
+
+ /**
+ * Decode a file path into a bitmap. If the specified file name is null,
+ * or cannot be decoded into a bitmap, the function returns null.
+ *
+ * @param pathName complete path name for the file to be decoded.
+ * @param opts null-ok; Options that control downsampling and whether the
+ * image should be completely decoded, or just is size returned.
+ * @return The decoded bitmap, or null if the image data could not be
+ * decoded, or, if opts is non-null, if opts requested only the
+ * size be returned (in opts.outWidth and opts.outHeight)
+ * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
+ * is {@link android.graphics.Bitmap.Config#HARDWARE}
+ * and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
+ * is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
+ * function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
+ */
+ public static Bitmap decodeFile(String pathName, Options opts) {
+ validate(opts);
+ Bitmap bm = null;
+ InputStream stream = null;
+ try {
+ stream = new FileInputStream(pathName);
+ bm = decodeStream(stream, null, opts);
+ } catch (Exception e) {
+ /* do nothing.
+ If the exception happened on open, bm will be null.
+ */
+ Log.e("BitmapFactory", "Unable to decode stream: " + e);
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // do nothing here
+ }
+ }
+ }
+ return bm;
+ }
+
+ /**
+ * Decode a file path into a bitmap. If the specified file name is null,
+ * or cannot be decoded into a bitmap, the function returns null.
+ *
+ * @param pathName complete path name for the file to be decoded.
+ * @return the resulting decoded bitmap, or null if it could not be decoded.
+ */
+ public static Bitmap decodeFile(String pathName) {
+ return decodeFile(pathName, null);
+ }
+
+ /**
+ * Decode a new Bitmap from an InputStream. This InputStream was obtained from
+ * resources, which we pass to be able to scale the bitmap accordingly.
+ * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
+ * is {@link android.graphics.Bitmap.Config#HARDWARE}
+ * and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
+ * is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
+ * function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
+ */
+ public static Bitmap decodeResourceStream(Resources res, TypedValue value,
+ InputStream is, Rect pad, Options opts) {
+ validate(opts);
+ if (opts == null) {
+ opts = new Options();
+ }
+
+ if (opts.inDensity == 0 && value != null) {
+ final int density = value.density;
+ if (density == TypedValue.DENSITY_DEFAULT) {
+ opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
+ } else if (density != TypedValue.DENSITY_NONE) {
+ opts.inDensity = density;
+ }
+ }
+
+ if (opts.inTargetDensity == 0 && res != null) {
+ opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
+ }
+
+ return decodeStream(is, pad, opts);
+ }
+
+ /**
+ * Synonym for opening the given resource and calling
+ * {@link #decodeResourceStream}.
+ *
+ * @param res The resources object containing the image data
+ * @param id The resource id of the image data
+ * @param opts null-ok; Options that control downsampling and whether the
+ * image should be completely decoded, or just is size returned.
+ * @return The decoded bitmap, or null if the image data could not be
+ * decoded, or, if opts is non-null, if opts requested only the
+ * size be returned (in opts.outWidth and opts.outHeight)
+ * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
+ * is {@link android.graphics.Bitmap.Config#HARDWARE}
+ * and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
+ * is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
+ * function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
+ */
+ public static Bitmap decodeResource(Resources res, int id, Options opts) {
+ validate(opts);
+ Bitmap bm = null;
+ InputStream is = null;
+
+ try {
+ final TypedValue value = new TypedValue();
+ is = res.openRawResource(id, value);
+
+ bm = decodeResourceStream(res, value, is, null, opts);
+ } catch (Exception e) {
+ /* do nothing.
+ If the exception happened on open, bm will be null.
+ If it happened on close, bm is still valid.
+ */
+ } finally {
+ try {
+ if (is != null) is.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+
+ if (bm == null && opts != null && opts.inBitmap != null) {
+ throw new IllegalArgumentException("Problem decoding into existing bitmap");
+ }
+
+ return bm;
+ }
+
+ /**
+ * Synonym for {@link #decodeResource(Resources, int, android.graphics.BitmapFactory.Options)}
+ * with null Options.
+ *
+ * @param res The resources object containing the image data
+ * @param id The resource id of the image data
+ * @return The decoded bitmap, or null if the image could not be decoded.
+ */
+ public static Bitmap decodeResource(Resources res, int id) {
+ return decodeResource(res, id, null);
+ }
+
+ /**
+ * Decode an immutable bitmap from the specified byte array.
+ *
+ * @param data byte array of compressed image data
+ * @param offset offset into imageData for where the decoder should begin
+ * parsing.
+ * @param length the number of bytes, beginning at offset, to parse
+ * @param opts null-ok; Options that control downsampling and whether the
+ * image should be completely decoded, or just is size returned.
+ * @return The decoded bitmap, or null if the image data could not be
+ * decoded, or, if opts is non-null, if opts requested only the
+ * size be returned (in opts.outWidth and opts.outHeight)
+ * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
+ * is {@link android.graphics.Bitmap.Config#HARDWARE}
+ * and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
+ * is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
+ * function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
+ */
+ public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {
+ if ((offset | length) < 0 || data.length < offset + length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ validate(opts);
+
+ Bitmap bm;
+
+ Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
+ try {
+ bm = nativeDecodeByteArray(data, offset, length, opts);
+
+ if (bm == null && opts != null && opts.inBitmap != null) {
+ throw new IllegalArgumentException("Problem decoding into existing bitmap");
+ }
+ setDensityFromOptions(bm, opts);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
+ }
+
+ return bm;
+ }
+
+ /**
+ * Decode an immutable bitmap from the specified byte array.
+ *
+ * @param data byte array of compressed image data
+ * @param offset offset into imageData for where the decoder should begin
+ * parsing.
+ * @param length the number of bytes, beginning at offset, to parse
+ * @return The decoded bitmap, or null if the image could not be decoded.
+ */
+ public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
+ return decodeByteArray(data, offset, length, null);
+ }
+
+ /**
+ * Set the newly decoded bitmap's density based on the Options.
+ */
+ private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
+ if (outputBitmap == null || opts == null) return;
+
+ final int density = opts.inDensity;
+ if (density != 0) {
+ outputBitmap.setDensity(density);
+ final int targetDensity = opts.inTargetDensity;
+ if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
+ return;
+ }
+
+ byte[] np = outputBitmap.getNinePatchChunk();
+ final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
+ if (opts.inScaled || isNinePatch) {
+ outputBitmap.setDensity(targetDensity);
+ }
+ } else if (opts.inBitmap != null) {
+ // bitmap was reused, ensure density is reset
+ outputBitmap.setDensity(Bitmap.getDefaultDensity());
+ }
+ }
+
+ /**
+ * Decode an input stream into a bitmap. If the input stream is null, or
+ * cannot be used to decode a bitmap, the function returns null.
+ * The stream's position will be where ever it was after the encoded data
+ * was read.
+ *
+ * @param is The input stream that holds the raw data to be decoded into a
+ * bitmap.
+ * @param outPadding If not null, return the padding rect for the bitmap if
+ * it exists, otherwise set padding to [-1,-1,-1,-1]. If
+ * no bitmap is returned (null) then padding is
+ * unchanged.
+ * @param opts null-ok; Options that control downsampling and whether the
+ * image should be completely decoded, or just is size returned.
+ * @return The decoded bitmap, or null if the image data could not be
+ * decoded, or, if opts is non-null, if opts requested only the
+ * size be returned (in opts.outWidth and opts.outHeight)
+ * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
+ * is {@link android.graphics.Bitmap.Config#HARDWARE}
+ * and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
+ * is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
+ * function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
+ *
+ * <p class="note">Prior to {@link android.os.Build.VERSION_CODES#KITKAT},
+ * if {@link InputStream#markSupported is.markSupported()} returns true,
+ * <code>is.mark(1024)</code> would be called. As of
+ * {@link android.os.Build.VERSION_CODES#KITKAT}, this is no longer the case.</p>
+ */
+ public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
+ // we don't throw in this case, thus allowing the caller to only check
+ // the cache, and not force the image to be decoded.
+ if (is == null) {
+ return null;
+ }
+ validate(opts);
+
+ Bitmap bm = null;
+
+ Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
+ try {
+ if (is instanceof AssetManager.AssetInputStream) {
+ final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
+ bm = nativeDecodeAsset(asset, outPadding, opts);
+ } else {
+ bm = decodeStreamInternal(is, outPadding, opts);
+ }
+
+ if (bm == null && opts != null && opts.inBitmap != null) {
+ throw new IllegalArgumentException("Problem decoding into existing bitmap");
+ }
+
+ setDensityFromOptions(bm, opts);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
+ }
+
+ return bm;
+ }
+
+ /**
+ * Private helper function for decoding an InputStream natively. Buffers the input enough to
+ * do a rewind as needed, and supplies temporary storage if necessary. is MUST NOT be null.
+ */
+ private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
+ // ASSERT(is != null);
+ byte [] tempStorage = null;
+ if (opts != null) tempStorage = opts.inTempStorage;
+ if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
+ return nativeDecodeStream(is, tempStorage, outPadding, opts);
+ }
+
+ /**
+ * Decode an input stream into a bitmap. If the input stream is null, or
+ * cannot be used to decode a bitmap, the function returns null.
+ * The stream's position will be where ever it was after the encoded data
+ * was read.
+ *
+ * @param is The input stream that holds the raw data to be decoded into a
+ * bitmap.
+ * @return The decoded bitmap, or null if the image data could not be decoded.
+ */
+ public static Bitmap decodeStream(InputStream is) {
+ return decodeStream(is, null, null);
+ }
+
+ /**
+ * Decode a bitmap from the file descriptor. If the bitmap cannot be decoded
+ * return null. The position within the descriptor will not be changed when
+ * this returns, so the descriptor can be used again as-is.
+ *
+ * @param fd The file descriptor containing the bitmap data to decode
+ * @param outPadding If not null, return the padding rect for the bitmap if
+ * it exists, otherwise set padding to [-1,-1,-1,-1]. If
+ * no bitmap is returned (null) then padding is
+ * unchanged.
+ * @param opts null-ok; Options that control downsampling and whether the
+ * image should be completely decoded, or just its size returned.
+ * @return the decoded bitmap, or null
+ * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
+ * is {@link android.graphics.Bitmap.Config#HARDWARE}
+ * and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
+ * is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
+ * function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
+ */
+ public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts) {
+ validate(opts);
+ Bitmap bm;
+
+ Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeFileDescriptor");
+ try {
+ if (nativeIsSeekable(fd)) {
+ bm = nativeDecodeFileDescriptor(fd, outPadding, opts);
+ } else {
+ FileInputStream fis = new FileInputStream(fd);
+ try {
+ bm = decodeStreamInternal(fis, outPadding, opts);
+ } finally {
+ try {
+ fis.close();
+ } catch (Throwable t) {/* ignore */}
+ }
+ }
+
+ if (bm == null && opts != null && opts.inBitmap != null) {
+ throw new IllegalArgumentException("Problem decoding into existing bitmap");
+ }
+
+ setDensityFromOptions(bm, opts);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
+ }
+ return bm;
+ }
+
+ /**
+ * Decode a bitmap from the file descriptor. If the bitmap cannot be decoded
+ * return null. The position within the descriptor will not be changed when
+ * this returns, so the descriptor can be used again as is.
+ *
+ * @param fd The file descriptor containing the bitmap data to decode
+ * @return the decoded bitmap, or null
+ */
+ public static Bitmap decodeFileDescriptor(FileDescriptor fd) {
+ return decodeFileDescriptor(fd, null, null);
+ }
+
+ private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
+ Rect padding, Options opts);
+ private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,
+ Rect padding, Options opts);
+ private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);
+ private static native Bitmap nativeDecodeByteArray(byte[] data, int offset,
+ int length, Options opts);
+ private static native boolean nativeIsSeekable(FileDescriptor fd);
+}
diff --git a/android/graphics/BitmapFactory_Delegate.java b/android/graphics/BitmapFactory_Delegate.java
new file mode 100644
index 0000000..8bd2a7a
--- /dev/null
+++ b/android/graphics/BitmapFactory_Delegate.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2011 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 android.graphics;
+
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.ninepatch.NinePatchChunk;
+import com.android.resources.Density;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.Nullable;
+import com.android.layoutlib.bridge.util.NinePatchInputStream;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.Bitmap_Delegate.BitmapCreateFlags;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.EnumSet;
+import java.util.Set;
+
+/**
+ * Delegate implementing the native methods of android.graphics.BitmapFactory
+ *
+ * Through the layoutlib_create tool, the original native methods of BitmapFactory have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager}
+ * around to map int to instance of the delegate.
+ *
+ */
+/*package*/ class BitmapFactory_Delegate {
+
+ // ------ Native Delegates ------
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeDecodeStream(InputStream is, byte[] storage,
+ @Nullable Rect padding, @Nullable Options opts) {
+ Bitmap bm = null;
+
+ Density density = Density.MEDIUM;
+ Set<BitmapCreateFlags> bitmapCreateFlags = EnumSet.of(BitmapCreateFlags.MUTABLE);
+ if (opts != null) {
+ density = Density.getEnum(opts.inDensity);
+ if (opts.inPremultiplied) {
+ bitmapCreateFlags.add(BitmapCreateFlags.PREMULTIPLIED);
+ }
+ opts.inScaled = false;
+ }
+
+ try {
+ if (is instanceof NinePatchInputStream) {
+ NinePatchInputStream npis = (NinePatchInputStream) is;
+ npis.disableFakeMarkSupport();
+
+ // load the bitmap as a nine patch
+ com.android.ninepatch.NinePatch ninePatch = com.android.ninepatch.NinePatch.load(
+ npis, true /*is9Patch*/, false /*convert*/);
+
+ // get the bitmap and chunk objects.
+ bm = Bitmap_Delegate.createBitmap(ninePatch.getImage(), bitmapCreateFlags,
+ density);
+ NinePatchChunk chunk = ninePatch.getChunk();
+
+ // put the chunk in the bitmap
+ bm.setNinePatchChunk(NinePatch_Delegate.serialize(chunk));
+
+ if (padding != null) {
+ // read the padding
+ int[] paddingArray = chunk.getPadding();
+ padding.left = paddingArray[0];
+ padding.top = paddingArray[1];
+ padding.right = paddingArray[2];
+ padding.bottom = paddingArray[3];
+ }
+ } else {
+ // load the bitmap directly.
+ bm = Bitmap_Delegate.createBitmap(is, bitmapCreateFlags, density);
+ }
+ } catch (IOException e) {
+ Bridge.getLog().error(null, "Failed to load image", e, null);
+ }
+
+ return bm;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,
+ Rect padding, Options opts) {
+ opts.inBitmap = null;
+ return null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeDecodeAsset(long asset, Rect padding, Options opts) {
+ opts.inBitmap = null;
+ return null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeDecodeByteArray(byte[] data, int offset,
+ int length, Options opts) {
+ opts.inBitmap = null;
+ return null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeIsSeekable(FileDescriptor fd) {
+ return true;
+ }
+
+ /**
+ * Set the newly decoded bitmap's density based on the Options.
+ *
+ * Copied from {@link BitmapFactory#setDensityFromOptions(Bitmap, Options)}.
+ */
+ @LayoutlibDelegate
+ /*package*/ static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
+ if (outputBitmap == null || opts == null) return;
+
+ final int density = opts.inDensity;
+ if (density != 0) {
+ outputBitmap.setDensity(density);
+ final int targetDensity = opts.inTargetDensity;
+ if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
+ return;
+ }
+
+ // --- Change from original implementation begins ---
+ // LayoutLib doesn't scale the nine patch when decoding it. Hence, don't change the
+ // density of the source bitmap in case of ninepatch.
+
+ if (opts.inScaled) {
+ // --- Change from original implementation ends. ---
+ outputBitmap.setDensity(targetDensity);
+ }
+ } else if (opts.inBitmap != null) {
+ // bitmap was reused, ensure density is reset
+ outputBitmap.setDensity(Bitmap.getDefaultDensity());
+ }
+ }
+}
diff --git a/android/graphics/BitmapRegionDecoder.java b/android/graphics/BitmapRegionDecoder.java
new file mode 100644
index 0000000..2da27c7
--- /dev/null
+++ b/android/graphics/BitmapRegionDecoder.java
@@ -0,0 +1,278 @@
+/* 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 android.graphics;
+
+import android.content.res.AssetManager;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * BitmapRegionDecoder can be used to decode a rectangle region from an image.
+ * BitmapRegionDecoder is particularly useful when an original image is large and
+ * you only need parts of the image.
+ *
+ * <p>To create a BitmapRegionDecoder, call newInstance(...).
+ * Given a BitmapRegionDecoder, users can call decodeRegion() repeatedly
+ * to get a decoded Bitmap of the specified region.
+ *
+ */
+public final class BitmapRegionDecoder {
+ private long mNativeBitmapRegionDecoder;
+ private boolean mRecycled;
+ // ensures that the native decoder object exists and that only one decode can
+ // occur at a time.
+ private final Object mNativeLock = new Object();
+
+ /**
+ * Create a BitmapRegionDecoder from the specified byte array.
+ * Currently only the JPEG and PNG formats are supported.
+ *
+ * @param data byte array of compressed image data.
+ * @param offset offset into data for where the decoder should begin
+ * parsing.
+ * @param length the number of bytes, beginning at offset, to parse
+ * @param isShareable If this is true, then the BitmapRegionDecoder may keep a
+ * shallow reference to the input. If this is false,
+ * then the BitmapRegionDecoder will explicitly make a copy of the
+ * input data, and keep that. Even if sharing is allowed,
+ * the implementation may still decide to make a deep
+ * copy of the input data. If an image is progressively encoded,
+ * allowing sharing may degrade the decoding speed.
+ * @return BitmapRegionDecoder, or null if the image data could not be decoded.
+ * @throws IOException if the image format is not supported or can not be decoded.
+ */
+ public static BitmapRegionDecoder newInstance(byte[] data,
+ int offset, int length, boolean isShareable) throws IOException {
+ if ((offset | length) < 0 || data.length < offset + length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ return nativeNewInstance(data, offset, length, isShareable);
+ }
+
+ /**
+ * Create a BitmapRegionDecoder from the file descriptor.
+ * The position within the descriptor will not be changed when
+ * this returns, so the descriptor can be used again as is.
+ * Currently only the JPEG and PNG formats are supported.
+ *
+ * @param fd The file descriptor containing the data to decode
+ * @param isShareable If this is true, then the BitmapRegionDecoder may keep a
+ * shallow reference to the input. If this is false,
+ * then the BitmapRegionDecoder will explicitly make a copy of the
+ * input data, and keep that. Even if sharing is allowed,
+ * the implementation may still decide to make a deep
+ * copy of the input data. If an image is progressively encoded,
+ * allowing sharing may degrade the decoding speed.
+ * @return BitmapRegionDecoder, or null if the image data could not be decoded.
+ * @throws IOException if the image format is not supported or can not be decoded.
+ */
+ public static BitmapRegionDecoder newInstance(
+ FileDescriptor fd, boolean isShareable) throws IOException {
+ return nativeNewInstance(fd, isShareable);
+ }
+
+ /**
+ * Create a BitmapRegionDecoder from an input stream.
+ * The stream's position will be where ever it was after the encoded data
+ * was read.
+ * Currently only the JPEG and PNG formats are supported.
+ *
+ * @param is The input stream that holds the raw data to be decoded into a
+ * BitmapRegionDecoder.
+ * @param isShareable If this is true, then the BitmapRegionDecoder may keep a
+ * shallow reference to the input. If this is false,
+ * then the BitmapRegionDecoder will explicitly make a copy of the
+ * input data, and keep that. Even if sharing is allowed,
+ * the implementation may still decide to make a deep
+ * copy of the input data. If an image is progressively encoded,
+ * allowing sharing may degrade the decoding speed.
+ * @return BitmapRegionDecoder, or null if the image data could not be decoded.
+ * @throws IOException if the image format is not supported or can not be decoded.
+ *
+ * <p class="note">Prior to {@link android.os.Build.VERSION_CODES#KITKAT},
+ * if {@link InputStream#markSupported is.markSupported()} returns true,
+ * <code>is.mark(1024)</code> would be called. As of
+ * {@link android.os.Build.VERSION_CODES#KITKAT}, this is no longer the case.</p>
+ */
+ public static BitmapRegionDecoder newInstance(InputStream is,
+ boolean isShareable) throws IOException {
+ if (is instanceof AssetManager.AssetInputStream) {
+ return nativeNewInstance(
+ ((AssetManager.AssetInputStream) is).getNativeAsset(),
+ isShareable);
+ } else {
+ // pass some temp storage down to the native code. 1024 is made up,
+ // but should be large enough to avoid too many small calls back
+ // into is.read(...).
+ byte [] tempStorage = new byte[16 * 1024];
+ return nativeNewInstance(is, tempStorage, isShareable);
+ }
+ }
+
+ /**
+ * Create a BitmapRegionDecoder from a file path.
+ * Currently only the JPEG and PNG formats are supported.
+ *
+ * @param pathName complete path name for the file to be decoded.
+ * @param isShareable If this is true, then the BitmapRegionDecoder may keep a
+ * shallow reference to the input. If this is false,
+ * then the BitmapRegionDecoder will explicitly make a copy of the
+ * input data, and keep that. Even if sharing is allowed,
+ * the implementation may still decide to make a deep
+ * copy of the input data. If an image is progressively encoded,
+ * allowing sharing may degrade the decoding speed.
+ * @return BitmapRegionDecoder, or null if the image data could not be decoded.
+ * @throws IOException if the image format is not supported or can not be decoded.
+ */
+ public static BitmapRegionDecoder newInstance(String pathName,
+ boolean isShareable) throws IOException {
+ BitmapRegionDecoder decoder = null;
+ InputStream stream = null;
+
+ try {
+ stream = new FileInputStream(pathName);
+ decoder = newInstance(stream, isShareable);
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // do nothing here
+ }
+ }
+ }
+ return decoder;
+ }
+
+ /* Private constructor that must receive an already allocated native
+ region decoder int (pointer).
+
+ This can be called from JNI code.
+ */
+ private BitmapRegionDecoder(long decoder) {
+ mNativeBitmapRegionDecoder = decoder;
+ mRecycled = false;
+ }
+
+ /**
+ * Decodes a rectangle region in the image specified by rect.
+ *
+ * @param rect The rectangle that specified the region to be decode.
+ * @param options null-ok; Options that control downsampling.
+ * inPurgeable is not supported.
+ * @return The decoded bitmap, or null if the image data could not be
+ * decoded.
+ * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
+ * is {@link android.graphics.Bitmap.Config#HARDWARE}
+ * and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
+ * is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
+ * function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
+ */
+ public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) {
+ BitmapFactory.Options.validate(options);
+ synchronized (mNativeLock) {
+ checkRecycled("decodeRegion called on recycled region decoder");
+ if (rect.right <= 0 || rect.bottom <= 0 || rect.left >= getWidth()
+ || rect.top >= getHeight())
+ throw new IllegalArgumentException("rectangle is outside the image");
+ return nativeDecodeRegion(mNativeBitmapRegionDecoder, rect.left, rect.top,
+ rect.right - rect.left, rect.bottom - rect.top, options);
+ }
+ }
+
+ /** Returns the original image's width */
+ public int getWidth() {
+ synchronized (mNativeLock) {
+ checkRecycled("getWidth called on recycled region decoder");
+ return nativeGetWidth(mNativeBitmapRegionDecoder);
+ }
+ }
+
+ /** Returns the original image's height */
+ public int getHeight() {
+ synchronized (mNativeLock) {
+ checkRecycled("getHeight called on recycled region decoder");
+ return nativeGetHeight(mNativeBitmapRegionDecoder);
+ }
+ }
+
+ /**
+ * Frees up the memory associated with this region decoder, and mark the
+ * region decoder as "dead", meaning it will throw an exception if decodeRegion(),
+ * getWidth() or getHeight() is called.
+ *
+ * <p>This operation cannot be reversed, so it should only be called if you are
+ * sure there are no further uses for the region decoder. This is an advanced call,
+ * and normally need not be called, since the normal GC process will free up this
+ * memory when there are no more references to this region decoder.
+ */
+ public void recycle() {
+ synchronized (mNativeLock) {
+ if (!mRecycled) {
+ nativeClean(mNativeBitmapRegionDecoder);
+ mRecycled = true;
+ }
+ }
+ }
+
+ /**
+ * Returns true if this region decoder has been recycled.
+ * If so, then it is an error to try use its method.
+ *
+ * @return true if the region decoder has been recycled
+ */
+ public final boolean isRecycled() {
+ return mRecycled;
+ }
+
+ /**
+ * Called by methods that want to throw an exception if the region decoder
+ * has already been recycled.
+ */
+ private void checkRecycled(String errorMessage) {
+ if (mRecycled) {
+ throw new IllegalStateException(errorMessage);
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ recycle();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private static native Bitmap nativeDecodeRegion(long lbm,
+ int start_x, int start_y, int width, int height,
+ BitmapFactory.Options options);
+ private static native int nativeGetWidth(long lbm);
+ private static native int nativeGetHeight(long lbm);
+ private static native void nativeClean(long lbm);
+
+ private static native BitmapRegionDecoder nativeNewInstance(
+ byte[] data, int offset, int length, boolean isShareable);
+ private static native BitmapRegionDecoder nativeNewInstance(
+ FileDescriptor fd, boolean isShareable);
+ private static native BitmapRegionDecoder nativeNewInstance(
+ InputStream is, byte[] storage, boolean isShareable);
+ private static native BitmapRegionDecoder nativeNewInstance(
+ long asset, boolean isShareable);
+}
diff --git a/android/graphics/BitmapShader.java b/android/graphics/BitmapShader.java
new file mode 100644
index 0000000..5577f53
--- /dev/null
+++ b/android/graphics/BitmapShader.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import android.annotation.NonNull;
+
+/**
+ * Shader used to draw a bitmap as a texture. The bitmap can be repeated or
+ * mirrored by setting the tiling mode.
+ */
+public class BitmapShader extends Shader {
+ /**
+ * Prevent garbage collection.
+ * @hide
+ */
+ @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
+ public Bitmap mBitmap;
+
+ private int mTileX;
+ private int mTileY;
+
+ /**
+ * Call this to create a new shader that will draw with a bitmap.
+ *
+ * @param bitmap The bitmap to use inside the shader
+ * @param tileX The tiling mode for x to draw the bitmap in.
+ * @param tileY The tiling mode for y to draw the bitmap in.
+ */
+ public BitmapShader(@NonNull Bitmap bitmap, @NonNull TileMode tileX, @NonNull TileMode tileY) {
+ this(bitmap, tileX.nativeInt, tileY.nativeInt);
+ }
+
+ private BitmapShader(Bitmap bitmap, int tileX, int tileY) {
+ if (bitmap == null) {
+ throw new IllegalArgumentException("Bitmap must be non-null");
+ }
+ if (bitmap == mBitmap && tileX == mTileX && tileY == mTileY) {
+ return;
+ }
+ mBitmap = bitmap;
+ mTileX = tileX;
+ mTileY = tileY;
+ }
+
+ @Override
+ long createNativeInstance(long nativeMatrix) {
+ return nativeCreate(nativeMatrix, mBitmap, mTileX, mTileY);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected Shader copy() {
+ final BitmapShader copy = new BitmapShader(mBitmap, mTileX, mTileY);
+ copyLocalMatrix(copy);
+ return copy;
+ }
+
+ private static native long nativeCreate(long nativeMatrix, Bitmap bitmap,
+ int shaderTileModeX, int shaderTileModeY);
+}
diff --git a/android/graphics/BitmapShader_Delegate.java b/android/graphics/BitmapShader_Delegate.java
new file mode 100644
index 0000000..891b07f
--- /dev/null
+++ b/android/graphics/BitmapShader_Delegate.java
@@ -0,0 +1,259 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.graphics.Shader.TileMode;
+
+import java.awt.PaintContext;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.NoninvertibleTransformException;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBufferInt;
+import java.awt.image.Raster;
+import java.awt.image.SampleModel;
+
+/**
+ * Delegate implementing the native methods of android.graphics.BitmapShader
+ *
+ * Through the layoutlib_create tool, the original native methods of BitmapShader have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original BitmapShader class.
+ *
+ * Because this extends {@link Shader_Delegate}, there's no need to use a {@link DelegateManager},
+ * as all the Shader classes will be added to the manager owned by {@link Shader_Delegate}.
+ *
+ * @see Shader_Delegate
+ *
+ */
+public class BitmapShader_Delegate extends Shader_Delegate {
+
+ // ---- delegate data ----
+ private java.awt.Paint mJavaPaint;
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public java.awt.Paint getJavaPaint() {
+ return mJavaPaint;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return true;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ // no message since isSupported returns true;
+ return null;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate(long nativeMatrix, Bitmap androidBitmap,
+ int shaderTileModeX, int shaderTileModeY) {
+ Bitmap_Delegate bitmap = Bitmap_Delegate.getDelegate(androidBitmap);
+ if (bitmap == null) {
+ return 0;
+ }
+
+ BitmapShader_Delegate newDelegate = new BitmapShader_Delegate(nativeMatrix,
+ bitmap.getImage(),
+ Shader_Delegate.getTileMode(shaderTileModeX),
+ Shader_Delegate.getTileMode(shaderTileModeY));
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+
+ private BitmapShader_Delegate(long matrix, BufferedImage image,
+ TileMode tileModeX, TileMode tileModeY) {
+ super(matrix);
+ mJavaPaint = new BitmapShaderPaint(image, tileModeX, tileModeY);
+ }
+
+ private class BitmapShaderPaint implements java.awt.Paint {
+ private final BufferedImage mImage;
+ private final TileMode mTileModeX;
+ private final TileMode mTileModeY;
+
+ BitmapShaderPaint(BufferedImage image,
+ TileMode tileModeX, TileMode tileModeY) {
+ mImage = image;
+ mTileModeX = tileModeX;
+ mTileModeY = tileModeY;
+ }
+
+ @Override
+ public PaintContext createContext(ColorModel colorModel, Rectangle deviceBounds,
+ Rectangle2D userBounds, AffineTransform xform, RenderingHints hints) {
+ AffineTransform canvasMatrix;
+ try {
+ canvasMatrix = xform.createInverse();
+ } catch (NoninvertibleTransformException e) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE,
+ "Unable to inverse matrix in BitmapShader", e, null /*data*/);
+ canvasMatrix = new AffineTransform();
+ }
+
+ AffineTransform localMatrix = getLocalMatrix();
+ try {
+ localMatrix = localMatrix.createInverse();
+ } catch (NoninvertibleTransformException e) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE,
+ "Unable to inverse matrix in BitmapShader", e, null /*data*/);
+ localMatrix = new AffineTransform();
+ }
+
+ if (!colorModel.isCompatibleRaster(mImage.getRaster())) {
+ // Fallback to the default ARGB color model
+ colorModel = ColorModel.getRGBdefault();
+ }
+
+ return new BitmapShaderContext(canvasMatrix, localMatrix, colorModel);
+ }
+
+ private class BitmapShaderContext implements PaintContext {
+
+ private final AffineTransform mCanvasMatrix;
+ private final AffineTransform mLocalMatrix;
+ private final ColorModel mColorModel;
+
+ public BitmapShaderContext(
+ AffineTransform canvasMatrix,
+ AffineTransform localMatrix,
+ ColorModel colorModel) {
+ mCanvasMatrix = canvasMatrix;
+ mLocalMatrix = localMatrix;
+ mColorModel = colorModel;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public ColorModel getColorModel() {
+ return mColorModel;
+ }
+
+ @Override
+ public Raster getRaster(int x, int y, int w, int h) {
+ BufferedImage image = new BufferedImage(
+ mColorModel, mColorModel.createCompatibleWritableRaster(w, h),
+ mColorModel.isAlphaPremultiplied(), null);
+
+ int[] data = new int[w*h];
+
+ int index = 0;
+ float[] pt1 = new float[2];
+ float[] pt2 = new float[2];
+ for (int iy = 0 ; iy < h ; iy++) {
+ for (int ix = 0 ; ix < w ; ix++) {
+ // handle the canvas transform
+ pt1[0] = x + ix;
+ pt1[1] = y + iy;
+ mCanvasMatrix.transform(pt1, 0, pt2, 0, 1);
+
+ // handle the local matrix.
+ pt1[0] = pt2[0];
+ pt1[1] = pt2[1];
+ mLocalMatrix.transform(pt1, 0, pt2, 0, 1);
+
+ data[index++] = getColor(pt2[0], pt2[1]);
+ }
+ }
+
+ DataBufferInt dataBuffer = new DataBufferInt(data, data.length);
+ SampleModel colorModel = mColorModel.createCompatibleSampleModel(w, h);
+ return Raster.createWritableRaster(colorModel, dataBuffer, null);
+ }
+ }
+
+ /**
+ * Returns a color for an arbitrary point.
+ */
+ private int getColor(float fx, float fy) {
+ int x = getCoordinate(Math.round(fx), mImage.getWidth(), mTileModeX);
+ int y = getCoordinate(Math.round(fy), mImage.getHeight(), mTileModeY);
+
+ return mImage.getRGB(x, y);
+ }
+
+ private int getCoordinate(int i, int size, TileMode mode) {
+ if (i < 0) {
+ switch (mode) {
+ case CLAMP:
+ i = 0;
+ break;
+ case REPEAT:
+ i = size - 1 - (-i % size);
+ break;
+ case MIRROR:
+ // this is the same as the positive side, just make the value positive
+ // first.
+ i = -i;
+ int count = i / size;
+ i = i % size;
+
+ if ((count % 2) == 1) {
+ i = size - 1 - i;
+ }
+ break;
+ }
+ } else if (i >= size) {
+ switch (mode) {
+ case CLAMP:
+ i = size - 1;
+ break;
+ case REPEAT:
+ i = i % size;
+ break;
+ case MIRROR:
+ int count = i / size;
+ i = i % size;
+
+ if ((count % 2) == 1) {
+ i = size - 1 - i;
+ }
+ break;
+ }
+ }
+
+ return i;
+ }
+
+
+ @Override
+ public int getTransparency() {
+ return java.awt.Paint.TRANSLUCENT;
+ }
+ }
+}
diff --git a/android/graphics/Bitmap_Delegate.java b/android/graphics/Bitmap_Delegate.java
new file mode 100644
index 0000000..0064537
--- /dev/null
+++ b/android/graphics/Bitmap_Delegate.java
@@ -0,0 +1,736 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.RenderResources;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.android.BridgeContext;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.layoutlib.bridge.impl.RenderAction;
+import com.android.resources.Density;
+import com.android.resources.ResourceType;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.Nullable;
+import android.graphics.Bitmap.Config;
+import android.os.Parcel;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.Buffer;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Set;
+
+import javax.imageio.ImageIO;
+import libcore.util.NativeAllocationRegistry_Delegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.Bitmap
+ *
+ * Through the layoutlib_create tool, the original native methods of Bitmap have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original Bitmap class.
+ *
+ * @see DelegateManager
+ *
+ */
+public final class Bitmap_Delegate {
+
+
+ public enum BitmapCreateFlags {
+ NONE, PREMULTIPLIED, MUTABLE
+ }
+
+ // ---- delegate manager ----
+ private static final DelegateManager<Bitmap_Delegate> sManager =
+ new DelegateManager<>(Bitmap_Delegate.class);
+ private static long sFinalizer = -1;
+
+ // ---- delegate helper data ----
+
+ // ---- delegate data ----
+ private final Config mConfig;
+ private final BufferedImage mImage;
+ private boolean mHasAlpha = true;
+ private boolean mHasMipMap = false; // TODO: check the default.
+ private boolean mIsPremultiplied = true;
+ private int mGenerationId = 0;
+
+
+ // ---- Public Helper methods ----
+
+ /**
+ * Returns the native delegate associated to a given an int referencing a {@link Bitmap} object.
+ */
+ public static Bitmap_Delegate getDelegate(long native_bitmap) {
+ return sManager.getDelegate(native_bitmap);
+ }
+
+ @Nullable
+ public static Bitmap_Delegate getDelegate(@Nullable Bitmap bitmap) {
+ return bitmap == null ? null : getDelegate(bitmap.getNativeInstance());
+ }
+
+ /**
+ * Creates and returns a {@link Bitmap} initialized with the given file content.
+ *
+ * @param input the file from which to read the bitmap content
+ * @param isMutable whether the bitmap is mutable
+ * @param density the density associated with the bitmap
+ *
+ * @see Bitmap#isMutable()
+ * @see Bitmap#getDensity()
+ */
+ public static Bitmap createBitmap(File input, boolean isMutable, Density density)
+ throws IOException {
+ return createBitmap(input, getPremultipliedBitmapCreateFlags(isMutable), density);
+ }
+
+ /**
+ * Creates and returns a {@link Bitmap} initialized with the given file content.
+ *
+ * @param input the file from which to read the bitmap content
+ * @param density the density associated with the bitmap
+ *
+ * @see Bitmap#isPremultiplied()
+ * @see Bitmap#isMutable()
+ * @see Bitmap#getDensity()
+ */
+ private static Bitmap createBitmap(File input, Set<BitmapCreateFlags> createFlags,
+ Density density) throws IOException {
+ // create a delegate with the content of the file.
+ BufferedImage image = ImageIO.read(input);
+ if (image == null && input.exists()) {
+ // There was a problem decoding the image, or the decoder isn't registered. Webp maybe.
+ // Replace with a broken image icon.
+ BridgeContext currentContext = RenderAction.getCurrentContext();
+ if (currentContext != null) {
+ RenderResources resources = currentContext.getRenderResources();
+ ResourceValue broken = resources.getFrameworkResource(ResourceType.DRAWABLE,
+ "ic_menu_report_image");
+ File brokenFile = new File(broken.getValue());
+ if (brokenFile.exists()) {
+ image = ImageIO.read(brokenFile);
+ }
+ }
+ }
+ Bitmap_Delegate delegate = new Bitmap_Delegate(image, Config.ARGB_8888);
+
+ return createBitmap(delegate, createFlags, density.getDpiValue());
+ }
+
+ /**
+ * Creates and returns a {@link Bitmap} initialized with the given stream content.
+ *
+ * @param input the stream from which to read the bitmap content
+ * @param isMutable whether the bitmap is mutable
+ * @param density the density associated with the bitmap
+ *
+ * @see Bitmap#isMutable()
+ * @see Bitmap#getDensity()
+ */
+ public static Bitmap createBitmap(InputStream input, boolean isMutable, Density density)
+ throws IOException {
+ return createBitmap(input, getPremultipliedBitmapCreateFlags(isMutable), density);
+ }
+
+ /**
+ * Creates and returns a {@link Bitmap} initialized with the given stream content.
+ *
+ * @param input the stream from which to read the bitmap content
+ * @param density the density associated with the bitmap
+ *
+ * @see Bitmap#isPremultiplied()
+ * @see Bitmap#isMutable()
+ * @see Bitmap#getDensity()
+ */
+ public static Bitmap createBitmap(InputStream input, Set<BitmapCreateFlags> createFlags,
+ Density density) throws IOException {
+ // create a delegate with the content of the stream.
+ Bitmap_Delegate delegate = new Bitmap_Delegate(ImageIO.read(input), Config.ARGB_8888);
+
+ return createBitmap(delegate, createFlags, density.getDpiValue());
+ }
+
+ /**
+ * Creates and returns a {@link Bitmap} initialized with the given {@link BufferedImage}
+ *
+ * @param image the bitmap content
+ * @param isMutable whether the bitmap is mutable
+ * @param density the density associated with the bitmap
+ *
+ * @see Bitmap#isMutable()
+ * @see Bitmap#getDensity()
+ */
+ public static Bitmap createBitmap(BufferedImage image, boolean isMutable, Density density) {
+ return createBitmap(image, getPremultipliedBitmapCreateFlags(isMutable), density);
+ }
+
+ /**
+ * Creates and returns a {@link Bitmap} initialized with the given {@link BufferedImage}
+ *
+ * @param image the bitmap content
+ * @param density the density associated with the bitmap
+ *
+ * @see Bitmap#isPremultiplied()
+ * @see Bitmap#isMutable()
+ * @see Bitmap#getDensity()
+ */
+ public static Bitmap createBitmap(BufferedImage image, Set<BitmapCreateFlags> createFlags,
+ Density density) {
+ // create a delegate with the given image.
+ Bitmap_Delegate delegate = new Bitmap_Delegate(image, Config.ARGB_8888);
+
+ return createBitmap(delegate, createFlags, density.getDpiValue());
+ }
+
+ private static int getBufferedImageType() {
+ return BufferedImage.TYPE_INT_ARGB;
+ }
+
+ /**
+ * Returns the {@link BufferedImage} used by the delegate of the given {@link Bitmap}.
+ */
+ public BufferedImage getImage() {
+ return mImage;
+ }
+
+ /**
+ * Returns the Android bitmap config. Note that this not the config of the underlying
+ * Java2D bitmap.
+ */
+ public Config getConfig() {
+ return mConfig;
+ }
+
+ /**
+ * Returns the hasAlpha rendering hint
+ * @return true if the bitmap alpha should be used at render time
+ */
+ public boolean hasAlpha() {
+ return mHasAlpha && mConfig != Config.RGB_565;
+ }
+
+ /**
+ * Update the generationId.
+ *
+ * @see Bitmap#getGenerationId()
+ */
+ public void change() {
+ mGenerationId++;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeCreate(int[] colors, int offset, int stride, int width,
+ int height, int nativeConfig, boolean isMutable, @Nullable float[] xyzD50,
+ @Nullable ColorSpace.Rgb.TransferParameters p) {
+ int imageType = getBufferedImageType();
+
+ // create the image
+ BufferedImage image = new BufferedImage(width, height, imageType);
+
+ if (colors != null) {
+ image.setRGB(0, 0, width, height, colors, offset, stride);
+ }
+
+ // create a delegate with the content of the stream.
+ Bitmap_Delegate delegate = new Bitmap_Delegate(image, Config.nativeToConfig(nativeConfig));
+
+ return createBitmap(delegate, getPremultipliedBitmapCreateFlags(isMutable),
+ Bitmap.getDefaultDensity());
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeCopy(long srcBitmap, int nativeConfig, boolean isMutable) {
+ Bitmap_Delegate srcBmpDelegate = sManager.getDelegate(srcBitmap);
+ if (srcBmpDelegate == null) {
+ return null;
+ }
+
+ BufferedImage srcImage = srcBmpDelegate.getImage();
+
+ int width = srcImage.getWidth();
+ int height = srcImage.getHeight();
+
+ int imageType = getBufferedImageType();
+
+ // create the image
+ BufferedImage image = new BufferedImage(width, height, imageType);
+
+ // copy the source image into the image.
+ int[] argb = new int[width * height];
+ srcImage.getRGB(0, 0, width, height, argb, 0, width);
+ image.setRGB(0, 0, width, height, argb, 0, width);
+
+ // create a delegate with the content of the stream.
+ Bitmap_Delegate delegate = new Bitmap_Delegate(image, Config.nativeToConfig(nativeConfig));
+
+ return createBitmap(delegate, getPremultipliedBitmapCreateFlags(isMutable),
+ Bitmap.getDefaultDensity());
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeCopyAshmem(long nativeSrcBitmap) {
+ // Unused method; no implementation provided.
+ assert false;
+ return null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeCopyAshmemConfig(long nativeSrcBitmap, int nativeConfig) {
+ // Unused method; no implementation provided.
+ assert false;
+ return null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeGetNativeFinalizer() {
+ synchronized (Bitmap_Delegate.class) {
+ if (sFinalizer == -1) {
+ sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(sManager::removeJavaReferenceFor);
+ }
+ return sFinalizer;
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeRecycle(long nativeBitmap) {
+ // In our case reycle() is a no-op. We will let the finalizer to dispose the bitmap.
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeReconfigure(long nativeBitmap, int width, int height,
+ int config, boolean isPremultiplied) {
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "Bitmap.reconfigure() is not supported", null /*data*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeCompress(long nativeBitmap, int format, int quality,
+ OutputStream stream, byte[] tempStorage) {
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "Bitmap.compress() is not supported", null /*data*/);
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeErase(long nativeBitmap, int color) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return;
+ }
+
+ BufferedImage image = delegate.mImage;
+
+ Graphics2D g = image.createGraphics();
+ try {
+ g.setColor(new java.awt.Color(color, true));
+
+ g.fillRect(0, 0, image.getWidth(), image.getHeight());
+ } finally {
+ g.dispose();
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nativeRowBytes(long nativeBitmap) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mImage.getWidth();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nativeConfig(long nativeBitmap) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mConfig.nativeInt;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeHasAlpha(long nativeBitmap) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ return delegate == null || delegate.mHasAlpha;
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeHasMipMap(long nativeBitmap) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ return delegate == null || delegate.mHasMipMap;
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nativeGetPixel(long nativeBitmap, int x, int y) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mImage.getRGB(x, y);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeGetPixels(long nativeBitmap, int[] pixels, int offset,
+ int stride, int x, int y, int width, int height) {
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.getImage().getRGB(x, y, width, height, pixels, offset, stride);
+ }
+
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeSetPixel(long nativeBitmap, int x, int y, int color) {
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.getImage().setRGB(x, y, color);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeSetPixels(long nativeBitmap, int[] colors, int offset,
+ int stride, int x, int y, int width, int height) {
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.getImage().setRGB(x, y, width, height, colors, offset, stride);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeCopyPixelsToBuffer(long nativeBitmap, Buffer dst) {
+ // FIXME implement native delegate
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Bitmap.copyPixelsToBuffer is not supported.", null, null /*data*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeCopyPixelsFromBuffer(long nb, Buffer src) {
+ // FIXME implement native delegate
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Bitmap.copyPixelsFromBuffer is not supported.", null, null /*data*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nativeGenerationId(long nativeBitmap) {
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mGenerationId;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeCreateFromParcel(Parcel p) {
+ // This is only called by Bitmap.CREATOR (Parcelable.Creator<Bitmap>), which is only
+ // used during aidl call so really this should not be called.
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "AIDL is not suppored, and therefore Bitmaps cannot be created from parcels.",
+ null /*data*/);
+ return null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeWriteToParcel(long nativeBitmap, boolean isMutable,
+ int density, Parcel p) {
+ // This is only called when sending a bitmap through aidl, so really this should not
+ // be called.
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "AIDL is not suppored, and therefore Bitmaps cannot be written to parcels.",
+ null /*data*/);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeExtractAlpha(long nativeBitmap, long nativePaint,
+ int[] offsetXY) {
+ Bitmap_Delegate bitmap = sManager.getDelegate(nativeBitmap);
+ if (bitmap == null) {
+ return null;
+ }
+
+ // get the paint which can be null if nativePaint is 0.
+ Paint_Delegate paint = Paint_Delegate.getDelegate(nativePaint);
+
+ if (paint != null && paint.getMaskFilter() != null) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MASKFILTER,
+ "MaskFilter not supported in Bitmap.extractAlpha",
+ null, null /*data*/);
+ }
+
+ int alpha = paint != null ? paint.getAlpha() : 0xFF;
+ BufferedImage image = createCopy(bitmap.getImage(), BufferedImage.TYPE_INT_ARGB, alpha);
+
+ // create the delegate. The actual Bitmap config is only an alpha channel
+ Bitmap_Delegate delegate = new Bitmap_Delegate(image, Config.ALPHA_8);
+
+ // the density doesn't matter, it's set by the Java method.
+ return createBitmap(delegate, EnumSet.of(BitmapCreateFlags.MUTABLE),
+ Density.DEFAULT_DENSITY /*density*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeIsPremultiplied(long nativeBitmap) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ return delegate != null && delegate.mIsPremultiplied;
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeSetPremultiplied(long nativeBitmap, boolean isPremul) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mIsPremultiplied = isPremul;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeSetHasAlpha(long nativeBitmap, boolean hasAlpha,
+ boolean isPremul) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mHasAlpha = hasAlpha;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeSetHasMipMap(long nativeBitmap, boolean hasMipMap) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mHasMipMap = hasMipMap;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeSameAs(long nb0, long nb1) {
+ Bitmap_Delegate delegate1 = sManager.getDelegate(nb0);
+ if (delegate1 == null) {
+ return false;
+ }
+
+ Bitmap_Delegate delegate2 = sManager.getDelegate(nb1);
+ if (delegate2 == null) {
+ return false;
+ }
+
+ BufferedImage image1 = delegate1.getImage();
+ BufferedImage image2 = delegate2.getImage();
+ if (delegate1.mConfig != delegate2.mConfig ||
+ image1.getWidth() != image2.getWidth() ||
+ image1.getHeight() != image2.getHeight()) {
+ return false;
+ }
+
+ // get the internal data
+ int w = image1.getWidth();
+ int h = image2.getHeight();
+ int[] argb1 = new int[w*h];
+ int[] argb2 = new int[w*h];
+
+ image1.getRGB(0, 0, w, h, argb1, 0, w);
+ image2.getRGB(0, 0, w, h, argb2, 0, w);
+
+ // compares
+ if (delegate1.mConfig == Config.ALPHA_8) {
+ // in this case we have to manually compare the alpha channel as the rest is garbage.
+ final int length = w*h;
+ for (int i = 0 ; i < length ; i++) {
+ if ((argb1[i] & 0xFF000000) != (argb2[i] & 0xFF000000)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return Arrays.equals(argb1, argb2);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nativeGetAllocationByteCount(long nativeBitmap) {
+ // get the delegate from the native int.
+ Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+ if (delegate == null) {
+ return 0;
+ }
+ return nativeRowBytes(nativeBitmap) * delegate.mImage.getHeight();
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativePrepareToDraw(long nativeBitmap) {
+ // do nothing as Bitmap_Delegate does not have caches
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeCopyPreserveInternalConfig(long nativeBitmap) {
+ Bitmap_Delegate srcBmpDelegate = sManager.getDelegate(nativeBitmap);
+ if (srcBmpDelegate == null) {
+ return null;
+ }
+
+ BufferedImage srcImage = srcBmpDelegate.getImage();
+
+ // create the image
+ BufferedImage image = new BufferedImage(srcImage.getColorModel(), srcImage.copyData(null),
+ srcImage.isAlphaPremultiplied(), null);
+
+ // create a delegate with the content of the stream.
+ Bitmap_Delegate delegate = new Bitmap_Delegate(image, srcBmpDelegate.getConfig());
+
+ return createBitmap(delegate, EnumSet.of(BitmapCreateFlags.NONE),
+ Bitmap.getDefaultDensity());
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Bitmap nativeCreateHardwareBitmap(GraphicBuffer buffer) {
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "Bitmap.nativeCreateHardwareBitmap() is not supported", null /*data*/);
+ return null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static GraphicBuffer nativeCreateGraphicBufferHandle(long nativeBitmap) {
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "Bitmap.nativeCreateGraphicBufferHandle() is not supported", null /*data*/);
+ return null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeIsSRGB(long nativeBitmap) {
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "Color spaces are not supported", null /*data*/);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeGetColorSpace(long nativePtr, float[] xyz, float[] params) {
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "Color spaces are not supported", null /*data*/);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeCopyColorSpace(long srcBitmap, long dstBitmap) {
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "Color spaces are not supported", null /*data*/);
+ }
+
+ // ---- Private delegate/helper methods ----
+
+ private Bitmap_Delegate(BufferedImage image, Config config) {
+ mImage = image;
+ mConfig = config;
+ }
+
+ private static Bitmap createBitmap(Bitmap_Delegate delegate,
+ Set<BitmapCreateFlags> createFlags, int density) {
+ // get its native_int
+ long nativeInt = sManager.addNewDelegate(delegate);
+
+ int width = delegate.mImage.getWidth();
+ int height = delegate.mImage.getHeight();
+ boolean isMutable = createFlags.contains(BitmapCreateFlags.MUTABLE);
+ boolean isPremultiplied = createFlags.contains(BitmapCreateFlags.PREMULTIPLIED);
+
+ // and create/return a new Bitmap with it
+ return new Bitmap(nativeInt, width, height, density, isMutable,
+ isPremultiplied, null /*ninePatchChunk*/, null /* layoutBounds */);
+ }
+
+ private static Set<BitmapCreateFlags> getPremultipliedBitmapCreateFlags(boolean isMutable) {
+ Set<BitmapCreateFlags> createFlags = EnumSet.of(BitmapCreateFlags.PREMULTIPLIED);
+ if (isMutable) {
+ createFlags.add(BitmapCreateFlags.MUTABLE);
+ }
+ return createFlags;
+ }
+
+ /**
+ * Creates and returns a copy of a given BufferedImage.
+ * <p/>
+ * if alpha is different than 255, then it is applied to the alpha channel of each pixel.
+ *
+ * @param image the image to copy
+ * @param imageType the type of the new image
+ * @param alpha an optional alpha modifier
+ * @return a new BufferedImage
+ */
+ /*package*/ static BufferedImage createCopy(BufferedImage image, int imageType, int alpha) {
+ int w = image.getWidth();
+ int h = image.getHeight();
+
+ BufferedImage result = new BufferedImage(w, h, imageType);
+
+ int[] argb = new int[w * h];
+ image.getRGB(0, 0, image.getWidth(), image.getHeight(), argb, 0, image.getWidth());
+
+ if (alpha != 255) {
+ final int length = argb.length;
+ for (int i = 0 ; i < length; i++) {
+ int a = (argb[i] >>> 24 * alpha) / 255;
+ argb[i] = (a << 24) | (argb[i] & 0x00FFFFFF);
+ }
+ }
+
+ result.setRGB(0, 0, w, h, argb, 0, w);
+
+ return result;
+ }
+
+}
diff --git a/android/graphics/BlendComposite.java b/android/graphics/BlendComposite.java
new file mode 100644
index 0000000..5cc964a
--- /dev/null
+++ b/android/graphics/BlendComposite.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2014 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 android.graphics;
+
+import java.awt.Composite;
+import java.awt.CompositeContext;
+import java.awt.RenderingHints;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+
+/*
+ * (non-Javadoc)
+ * The class is adapted from a demo tool for Blending Modes written by
+ * Romain Guy ([email protected]). The tool is available at
+ * http://www.curious-creature.org/2006/09/20/new-blendings-modes-for-java2d/
+ *
+ * This class has been adapted for applying color filters. When applying color filters, the src
+ * image should not extend beyond the dest image, but in our implementation of the filters, it does.
+ * To compensate for the effect, we recompute the alpha value of the src image before applying
+ * the color filter as it should have been applied.
+ */
+public final class BlendComposite implements Composite {
+ public enum BlendingMode {
+ MULTIPLY(),
+ SCREEN(),
+ DARKEN(),
+ LIGHTEN(),
+ OVERLAY(),
+ ADD();
+
+ private final BlendComposite mComposite;
+
+ BlendingMode() {
+ mComposite = new BlendComposite(this);
+ }
+
+ BlendComposite getBlendComposite() {
+ return mComposite;
+ }
+ }
+
+ private float alpha;
+ private BlendingMode mode;
+
+ private BlendComposite(BlendingMode mode) {
+ this(mode, 1.0f);
+ }
+
+ private BlendComposite(BlendingMode mode, float alpha) {
+ this.mode = mode;
+ setAlpha(alpha);
+ }
+
+ public static BlendComposite getInstance(BlendingMode mode) {
+ return mode.getBlendComposite();
+ }
+
+ public static BlendComposite getInstance(BlendingMode mode, float alpha) {
+ if (alpha > 0.9999f) {
+ return getInstance(mode);
+ }
+ return new BlendComposite(mode, alpha);
+ }
+
+ public float getAlpha() {
+ return alpha;
+ }
+
+ public BlendingMode getMode() {
+ return mode;
+ }
+
+ private void setAlpha(float alpha) {
+ if (alpha < 0.0f || alpha > 1.0f) {
+ assert false : "alpha must be comprised between 0.0f and 1.0f";
+ alpha = Math.min(alpha, 1.0f);
+ alpha = Math.max(alpha, 0.0f);
+ }
+
+ this.alpha = alpha;
+ }
+
+ @Override
+ public int hashCode() {
+ return Float.floatToIntBits(alpha) * 31 + mode.ordinal();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof BlendComposite)) {
+ return false;
+ }
+
+ BlendComposite bc = (BlendComposite) obj;
+
+ return mode == bc.mode && alpha == bc.alpha;
+ }
+
+ public CompositeContext createContext(ColorModel srcColorModel,
+ ColorModel dstColorModel,
+ RenderingHints hints) {
+ return new BlendingContext(this);
+ }
+
+ private static final class BlendingContext implements CompositeContext {
+ private final Blender blender;
+ private final BlendComposite composite;
+
+ private BlendingContext(BlendComposite composite) {
+ this.composite = composite;
+ this.blender = Blender.getBlenderFor(composite);
+ }
+
+ public void dispose() {
+ }
+
+ public void compose(Raster src, Raster dstIn, WritableRaster dstOut) {
+ if (src.getSampleModel().getDataType() != DataBuffer.TYPE_INT ||
+ dstIn.getSampleModel().getDataType() != DataBuffer.TYPE_INT ||
+ dstOut.getSampleModel().getDataType() != DataBuffer.TYPE_INT) {
+ throw new IllegalStateException(
+ "Source and destination must store pixels as INT.");
+ }
+
+ int width = Math.min(src.getWidth(), dstIn.getWidth());
+ int height = Math.min(src.getHeight(), dstIn.getHeight());
+
+ float alpha = composite.getAlpha();
+
+ int[] srcPixel = new int[4];
+ int[] dstPixel = new int[4];
+ int[] result = new int[4];
+ int[] srcPixels = new int[width];
+ int[] dstPixels = new int[width];
+
+ for (int y = 0; y < height; y++) {
+ dstIn.getDataElements(0, y, width, 1, dstPixels);
+ if (alpha != 0) {
+ src.getDataElements(0, y, width, 1, srcPixels);
+ for (int x = 0; x < width; x++) {
+ // pixels are stored as INT_ARGB
+ // our arrays are [R, G, B, A]
+ int pixel = srcPixels[x];
+ srcPixel[0] = (pixel >> 16) & 0xFF;
+ srcPixel[1] = (pixel >> 8) & 0xFF;
+ srcPixel[2] = (pixel ) & 0xFF;
+ srcPixel[3] = (pixel >> 24) & 0xFF;
+
+ pixel = dstPixels[x];
+ dstPixel[0] = (pixel >> 16) & 0xFF;
+ dstPixel[1] = (pixel >> 8) & 0xFF;
+ dstPixel[2] = (pixel ) & 0xFF;
+ dstPixel[3] = (pixel >> 24) & 0xFF;
+
+ // ---- Modified from original ----
+ // recompute src pixel for transparency.
+ srcPixel[3] *= dstPixel[3] / 0xFF;
+ // ---- Modification ends ----
+
+ result = blender.blend(srcPixel, dstPixel, result);
+
+ // mixes the result with the opacity
+ if (alpha == 1) {
+ dstPixels[x] = (result[3] & 0xFF) << 24 |
+ (result[0] & 0xFF) << 16 |
+ (result[1] & 0xFF) << 8 |
+ result[2] & 0xFF;
+ } else {
+ dstPixels[x] =
+ ((int) (dstPixel[3] + (result[3] - dstPixel[3]) * alpha) & 0xFF) << 24 |
+ ((int) (dstPixel[0] + (result[0] - dstPixel[0]) * alpha) & 0xFF) << 16 |
+ ((int) (dstPixel[1] + (result[1] - dstPixel[1]) * alpha) & 0xFF) << 8 |
+ (int) (dstPixel[2] + (result[2] - dstPixel[2]) * alpha) & 0xFF;
+ }
+
+ }
+ }
+ dstOut.setDataElements(0, y, width, 1, dstPixels);
+ }
+ }
+ }
+
+ private static abstract class Blender {
+ public abstract int[] blend(int[] src, int[] dst, int[] result);
+
+ public static Blender getBlenderFor(BlendComposite composite) {
+ switch (composite.getMode()) {
+ case ADD:
+ return new Blender() {
+ @Override
+ public int[] blend(int[] src, int[] dst, int[] result) {
+ for (int i = 0; i < 4; i++) {
+ result[i] = Math.min(255, src[i] + dst[i]);
+ }
+ return result;
+ }
+ };
+ case DARKEN:
+ return new Blender() {
+ @Override
+ public int[] blend(int[] src, int[] dst, int[] result) {
+ for (int i = 0; i < 3; i++) {
+ result[i] = Math.min(src[i], dst[i]);
+ }
+ result[3] = Math.min(255, src[3] + dst[3]);
+ return result;
+ }
+ };
+ case LIGHTEN:
+ return new Blender() {
+ @Override
+ public int[] blend(int[] src, int[] dst, int[] result) {
+ for (int i = 0; i < 3; i++) {
+ result[i] = Math.max(src[i], dst[i]);
+ }
+ result[3] = Math.min(255, src[3] + dst[3]);
+ return result;
+ }
+ };
+ case MULTIPLY:
+ return new Blender() {
+ @Override
+ public int[] blend(int[] src, int[] dst, int[] result) {
+ for (int i = 0; i < 3; i++) {
+ result[i] = (src[i] * dst[i]) >> 8;
+ }
+ result[3] = Math.min(255, src[3] + dst[3] - (src[3] * dst[3]) / 255);
+ return result;
+ }
+ };
+ case OVERLAY:
+ return new Blender() {
+ @Override
+ public int[] blend(int[] src, int[] dst, int[] result) {
+ for (int i = 0; i < 3; i++) {
+ result[i] = dst[i] < 128 ? dst[i] * src[i] >> 7 :
+ 255 - ((255 - dst[i]) * (255 - src[i]) >> 7);
+ }
+ result[3] = Math.min(255, src[3] + dst[3]);
+ return result;
+ }
+ };
+ case SCREEN:
+ return new Blender() {
+ @Override
+ public int[] blend(int[] src, int[] dst, int[] result) {
+ result[0] = 255 - ((255 - src[0]) * (255 - dst[0]) >> 8);
+ result[1] = 255 - ((255 - src[1]) * (255 - dst[1]) >> 8);
+ result[2] = 255 - ((255 - src[2]) * (255 - dst[2]) >> 8);
+ result[3] = Math.min(255, src[3] + dst[3]);
+ return result;
+ }
+ };
+ default:
+ assert false : "Blender not implement for " + composite.getMode().name();
+
+ // Ignore the blend
+ return new Blender() {
+ @Override
+ public int[] blend(int[] src, int[] dst, int[] result) {
+ result[0] = dst[0];
+ result[1] = dst[1];
+ result[2] = dst[2];
+ result[3] = dst[3];
+ return result;
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/android/graphics/BlurMaskFilter.java b/android/graphics/BlurMaskFilter.java
new file mode 100644
index 0000000..f3064f8
--- /dev/null
+++ b/android/graphics/BlurMaskFilter.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * This takes a mask, and blurs its edge by the specified radius. Whether or
+ * or not to include the original mask, and whether the blur goes outside,
+ * inside, or straddles, the original mask's border, is controlled by the
+ * Blur enum.
+ */
+public class BlurMaskFilter extends MaskFilter {
+
+ public enum Blur {
+ /**
+ * Blur inside and outside the original border.
+ */
+ NORMAL(0),
+
+ /**
+ * Draw solid inside the border, blur outside.
+ */
+ SOLID(1),
+
+ /**
+ * Draw nothing inside the border, blur outside.
+ */
+ OUTER(2),
+
+ /**
+ * Blur inside the border, draw nothing outside.
+ */
+ INNER(3);
+
+ Blur(int value) {
+ native_int = value;
+ }
+ final int native_int;
+ }
+
+ /**
+ * Create a blur maskfilter.
+ *
+ * @param radius The radius to extend the blur from the original mask. Must be > 0.
+ * @param style The Blur to use
+ * @return The new blur maskfilter
+ */
+ public BlurMaskFilter(float radius, Blur style) {
+ native_instance = nativeConstructor(radius, style.native_int);
+ }
+
+ private static native long nativeConstructor(float radius, int style);
+}
diff --git a/android/graphics/BlurMaskFilter_Delegate.java b/android/graphics/BlurMaskFilter_Delegate.java
new file mode 100644
index 0000000..d2569c7
--- /dev/null
+++ b/android/graphics/BlurMaskFilter_Delegate.java
@@ -0,0 +1,64 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.BlurMaskFilter
+ *
+ * Through the layoutlib_create tool, the original native methods of BlurMaskFilter have
+ * been replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original BlurMaskFilter class.
+ *
+ * Because this extends {@link MaskFilter_Delegate}, there's no need to use a
+ * {@link DelegateManager}, as all the Shader classes will be added to the manager
+ * owned by {@link MaskFilter_Delegate}.
+ *
+ * @see MaskFilter_Delegate
+ *
+ */
+public class BlurMaskFilter_Delegate extends MaskFilter_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public boolean isSupported() {
+ return false;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ return "Blur Mask Filters are not supported.";
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeConstructor(float radius, int style) {
+ BlurMaskFilter_Delegate newDelegate = new BlurMaskFilter_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/Camera.java b/android/graphics/Camera.java
new file mode 100644
index 0000000..60588d0
--- /dev/null
+++ b/android/graphics/Camera.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * A camera instance can be used to compute 3D transformations and
+ * generate a matrix that can be applied, for instance, on a
+ * {@link Canvas}.
+ */
+public class Camera {
+ private Matrix mMatrix;
+
+ /**
+ * Creates a new camera, with empty transformations.
+ */
+ public Camera() {
+ nativeConstructor();
+ }
+
+ /**
+ * Saves the camera state. Each save should be balanced
+ * with a call to {@link #restore()}.
+ *
+ * @see #save()
+ */
+ public native void save();
+
+ /**
+ * Restores the saved state, if any.
+ *
+ * @see #restore()
+ */
+ public native void restore();
+
+ /**
+ * Applies a translation transform on all three axis.
+ *
+ * @param x The distance to translate by on the X axis
+ * @param y The distance to translate by on the Y axis
+ * @param z The distance to translate by on the Z axis
+ */
+ public native void translate(float x, float y, float z);
+
+ /**
+ * Applies a rotation transform around the X axis.
+ *
+ * @param deg The angle of rotation around the X axis, in degrees
+ *
+ * @see #rotateY(float)
+ * @see #rotateZ(float)
+ * @see #rotate(float, float, float)
+ */
+ public native void rotateX(float deg);
+
+ /**
+ * Applies a rotation transform around the Y axis.
+ *
+ * @param deg The angle of rotation around the Y axis, in degrees
+ *
+ * @see #rotateX(float)
+ * @see #rotateZ(float)
+ * @see #rotate(float, float, float)
+ */
+ public native void rotateY(float deg);
+
+ /**
+ * Applies a rotation transform around the Z axis.
+ *
+ * @param deg The angle of rotation around the Z axis, in degrees
+ *
+ * @see #rotateX(float)
+ * @see #rotateY(float)
+ * @see #rotate(float, float, float)
+ */
+ public native void rotateZ(float deg);
+
+ /**
+ * Applies a rotation transform around all three axis.
+ *
+ * @param x The angle of rotation around the X axis, in degrees
+ * @param y The angle of rotation around the Y axis, in degrees
+ * @param z The angle of rotation around the Z axis, in degrees
+ *
+ * @see #rotateX(float)
+ * @see #rotateY(float)
+ * @see #rotateZ(float)
+ */
+ public native void rotate(float x, float y, float z);
+
+ /**
+ * Gets the x location of the camera.
+ *
+ * @see #setLocation(float, float, float)
+ */
+ public native float getLocationX();
+
+ /**
+ * Gets the y location of the camera.
+ *
+ * @see #setLocation(float, float, float)
+ */
+ public native float getLocationY();
+
+ /**
+ * Gets the z location of the camera.
+ *
+ * @see #setLocation(float, float, float)
+ */
+ public native float getLocationZ();
+
+ /**
+ * Sets the location of the camera. The default location is set at
+ * 0, 0, -8.
+ *
+ * @param x The x location of the camera
+ * @param y The y location of the camera
+ * @param z The z location of the camera
+ */
+ public native void setLocation(float x, float y, float z);
+
+ /**
+ * Computes the matrix corresponding to the current transformation
+ * and copies it to the supplied matrix object.
+ *
+ * @param matrix The matrix to copy the current transforms into
+ */
+ public void getMatrix(Matrix matrix) {
+ nativeGetMatrix(matrix.native_instance);
+ }
+
+ /**
+ * Computes the matrix corresponding to the current transformation
+ * and applies it to the specified Canvas.
+ *
+ * @param canvas The Canvas to set the transform matrix onto
+ */
+ public void applyToCanvas(Canvas canvas) {
+ if (canvas.isHardwareAccelerated()) {
+ if (mMatrix == null) mMatrix = new Matrix();
+ getMatrix(mMatrix);
+ canvas.concat(mMatrix);
+ } else {
+ nativeApplyToCanvas(canvas.getNativeCanvasWrapper());
+ }
+ }
+
+ public native float dotWithNormal(float dx, float dy, float dz);
+
+ protected void finalize() throws Throwable {
+ try {
+ nativeDestructor();
+ native_instance = 0;
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private native void nativeConstructor();
+ private native void nativeDestructor();
+ private native void nativeGetMatrix(long native_matrix);
+ private native void nativeApplyToCanvas(long native_canvas);
+
+ long native_instance;
+}
diff --git a/android/graphics/Canvas.java b/android/graphics/Canvas.java
new file mode 100644
index 0000000..f5e8633
--- /dev/null
+++ b/android/graphics/Canvas.java
@@ -0,0 +1,1978 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.os.Build;
+
+import dalvik.annotation.optimization.CriticalNative;
+import dalvik.annotation.optimization.FastNative;
+
+import libcore.util.NativeAllocationRegistry;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.microedition.khronos.opengles.GL;
+
+/**
+ * The Canvas class holds the "draw" calls. To draw something, you need
+ * 4 basic components: A Bitmap to hold the pixels, a Canvas to host
+ * the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect,
+ * Path, text, Bitmap), and a paint (to describe the colors and styles for the
+ * drawing).
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about how to use Canvas, read the
+ * <a href="{@docRoot}guide/topics/graphics/2d-graphics.html">
+ * Canvas and Drawables</a> developer guide.</p></div>
+ */
+public class Canvas extends BaseCanvas {
+ /** @hide */
+ public static boolean sCompatibilityRestore = false;
+ /** @hide */
+ public static boolean sCompatibilitySetBitmap = false;
+
+ /** @hide */
+ public long getNativeCanvasWrapper() {
+ return mNativeCanvasWrapper;
+ }
+
+ /** @hide */
+ public boolean isRecordingFor(Object o) { return false; }
+
+ // may be null
+ private Bitmap mBitmap;
+
+ // optional field set by the caller
+ private DrawFilter mDrawFilter;
+
+ // Maximum bitmap size as defined in Skia's native code
+ // (see SkCanvas.cpp, SkDraw.cpp)
+ private static final int MAXMIMUM_BITMAP_SIZE = 32766;
+
+ // The approximate size of the native allocation associated with
+ // a Canvas object.
+ private static final long NATIVE_ALLOCATION_SIZE = 525;
+
+ // Use a Holder to allow static initialization of Canvas in the boot image.
+ private static class NoImagePreloadHolder {
+ public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
+ Canvas.class.getClassLoader(), nGetNativeFinalizer(), NATIVE_ALLOCATION_SIZE);
+ }
+
+ // This field is used to finalize the native Canvas properly
+ private Runnable mFinalizer;
+
+ /**
+ * Construct an empty raster canvas. Use setBitmap() to specify a bitmap to
+ * draw into. The initial target density is {@link Bitmap#DENSITY_NONE};
+ * this will typically be replaced when a target bitmap is set for the
+ * canvas.
+ */
+ public Canvas() {
+ if (!isHardwareAccelerated()) {
+ // 0 means no native bitmap
+ mNativeCanvasWrapper = nInitRaster(null);
+ mFinalizer = NoImagePreloadHolder.sRegistry.registerNativeAllocation(
+ this, mNativeCanvasWrapper);
+ } else {
+ mFinalizer = null;
+ }
+ }
+
+ /**
+ * Construct a canvas with the specified bitmap to draw into. The bitmap
+ * must be mutable.
+ *
+ * <p>The initial target density of the canvas is the same as the given
+ * bitmap's density.
+ *
+ * @param bitmap Specifies a mutable bitmap for the canvas to draw into.
+ */
+ public Canvas(@NonNull Bitmap bitmap) {
+ if (!bitmap.isMutable()) {
+ throw new IllegalStateException("Immutable bitmap passed to Canvas constructor");
+ }
+ throwIfCannotDraw(bitmap);
+ mNativeCanvasWrapper = nInitRaster(bitmap);
+ mFinalizer = NoImagePreloadHolder.sRegistry.registerNativeAllocation(
+ this, mNativeCanvasWrapper);
+ mBitmap = bitmap;
+ mDensity = bitmap.mDensity;
+ }
+
+ /** @hide */
+ public Canvas(long nativeCanvas) {
+ if (nativeCanvas == 0) {
+ throw new IllegalStateException();
+ }
+ mNativeCanvasWrapper = nativeCanvas;
+ mFinalizer = NoImagePreloadHolder.sRegistry.registerNativeAllocation(
+ this, mNativeCanvasWrapper);
+ mDensity = Bitmap.getDefaultDensity();
+ }
+
+ /**
+ * Returns null.
+ *
+ * @deprecated This method is not supported and should not be invoked.
+ *
+ * @hide
+ */
+ @Deprecated
+ protected GL getGL() {
+ return null;
+ }
+
+ /**
+ * Indicates whether this Canvas uses hardware acceleration.
+ *
+ * Note that this method does not define what type of hardware acceleration
+ * may or may not be used.
+ *
+ * @return True if drawing operations are hardware accelerated,
+ * false otherwise.
+ */
+ public boolean isHardwareAccelerated() {
+ return false;
+ }
+
+ /**
+ * Specify a bitmap for the canvas to draw into. All canvas state such as
+ * layers, filters, and the save/restore stack are reset. Additionally,
+ * the canvas' target density is updated to match that of the bitmap.
+ *
+ * Prior to API level {@value Build.VERSION_CODES#O} the current matrix and
+ * clip stack were preserved.
+ *
+ * @param bitmap Specifies a mutable bitmap for the canvas to draw into.
+ * @see #setDensity(int)
+ * @see #getDensity()
+ */
+ public void setBitmap(@Nullable Bitmap bitmap) {
+ if (isHardwareAccelerated()) {
+ throw new RuntimeException("Can't set a bitmap device on a HW accelerated canvas");
+ }
+
+ Matrix preservedMatrix = null;
+ if (bitmap != null && sCompatibilitySetBitmap) {
+ preservedMatrix = getMatrix();
+ }
+
+ if (bitmap == null) {
+ nSetBitmap(mNativeCanvasWrapper, null);
+ mDensity = Bitmap.DENSITY_NONE;
+ } else {
+ if (!bitmap.isMutable()) {
+ throw new IllegalStateException();
+ }
+ throwIfCannotDraw(bitmap);
+
+ nSetBitmap(mNativeCanvasWrapper, bitmap);
+ mDensity = bitmap.mDensity;
+ }
+
+ if (preservedMatrix != null) {
+ setMatrix(preservedMatrix);
+ }
+
+ mBitmap = bitmap;
+ }
+
+ /** @hide */
+ public void insertReorderBarrier() {}
+
+ /** @hide */
+ public void insertInorderBarrier() {}
+
+ /**
+ * Return true if the device that the current layer draws into is opaque
+ * (i.e. does not support per-pixel alpha).
+ *
+ * @return true if the device that the current layer draws into is opaque
+ */
+ public boolean isOpaque() {
+ return nIsOpaque(mNativeCanvasWrapper);
+ }
+
+ /**
+ * Returns the width of the current drawing layer
+ *
+ * @return the width of the current drawing layer
+ */
+ public int getWidth() {
+ return nGetWidth(mNativeCanvasWrapper);
+ }
+
+ /**
+ * Returns the height of the current drawing layer
+ *
+ * @return the height of the current drawing layer
+ */
+ public int getHeight() {
+ return nGetHeight(mNativeCanvasWrapper);
+ }
+
+ /**
+ * <p>Returns the target density of the canvas. The default density is
+ * derived from the density of its backing bitmap, or
+ * {@link Bitmap#DENSITY_NONE} if there is not one.</p>
+ *
+ * @return Returns the current target density of the canvas, which is used
+ * to determine the scaling factor when drawing a bitmap into it.
+ *
+ * @see #setDensity(int)
+ * @see Bitmap#getDensity()
+ */
+ public int getDensity() {
+ return mDensity;
+ }
+
+ /**
+ * <p>Specifies the density for this Canvas' backing bitmap. This modifies
+ * the target density of the canvas itself, as well as the density of its
+ * backing bitmap via {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}.
+ *
+ * @param density The new target density of the canvas, which is used
+ * to determine the scaling factor when drawing a bitmap into it. Use
+ * {@link Bitmap#DENSITY_NONE} to disable bitmap scaling.
+ *
+ * @see #getDensity()
+ * @see Bitmap#setDensity(int)
+ */
+ public void setDensity(int density) {
+ if (mBitmap != null) {
+ mBitmap.setDensity(density);
+ }
+ mDensity = density;
+ }
+
+ /** @hide */
+ public void setScreenDensity(int density) {
+ mScreenDensity = density;
+ }
+
+ /**
+ * Returns the maximum allowed width for bitmaps drawn with this canvas.
+ * Attempting to draw with a bitmap wider than this value will result
+ * in an error.
+ *
+ * @see #getMaximumBitmapHeight()
+ */
+ public int getMaximumBitmapWidth() {
+ return MAXMIMUM_BITMAP_SIZE;
+ }
+
+ /**
+ * Returns the maximum allowed height for bitmaps drawn with this canvas.
+ * Attempting to draw with a bitmap taller than this value will result
+ * in an error.
+ *
+ * @see #getMaximumBitmapWidth()
+ */
+ public int getMaximumBitmapHeight() {
+ return MAXMIMUM_BITMAP_SIZE;
+ }
+
+ // the SAVE_FLAG constants must match their native equivalents
+
+ /** @hide */
+ @IntDef(flag = true,
+ value = {
+ ALL_SAVE_FLAG
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Saveflags {}
+
+ /**
+ * Restore the current matrix when restore() is called.
+ *
+ * @deprecated Use the flagless version of {@link #save()}, {@link #saveLayer(RectF, Paint)} or
+ * {@link #saveLayerAlpha(RectF, int)}. For saveLayer() calls the matrix
+ * was always restored for {@link #isHardwareAccelerated() Hardware accelerated}
+ * canvases and as of API level {@value Build.VERSION_CODES#O} that is the default
+ * behavior for all canvas types.
+ */
+ public static final int MATRIX_SAVE_FLAG = 0x01;
+
+ /**
+ * Restore the current clip when restore() is called.
+ *
+ * @deprecated Use the flagless version of {@link #save()}, {@link #saveLayer(RectF, Paint)} or
+ * {@link #saveLayerAlpha(RectF, int)}. For saveLayer() calls the clip
+ * was always restored for {@link #isHardwareAccelerated() Hardware accelerated}
+ * canvases and as of API level {@value Build.VERSION_CODES#O} that is the default
+ * behavior for all canvas types.
+ */
+ public static final int CLIP_SAVE_FLAG = 0x02;
+
+ /**
+ * The layer requires a per-pixel alpha channel.
+ *
+ * @deprecated This flag is ignored. Use the flagless version of {@link #saveLayer(RectF, Paint)}
+ * {@link #saveLayerAlpha(RectF, int)}.
+ */
+ public static final int HAS_ALPHA_LAYER_SAVE_FLAG = 0x04;
+
+ /**
+ * The layer requires full 8-bit precision for each color channel.
+ *
+ * @deprecated This flag is ignored. Use the flagless version of {@link #saveLayer(RectF, Paint)}
+ * {@link #saveLayerAlpha(RectF, int)}.
+ */
+ public static final int FULL_COLOR_LAYER_SAVE_FLAG = 0x08;
+
+ /**
+ * Clip drawing to the bounds of the offscreen layer, omit at your own peril.
+ * <p class="note"><strong>Note:</strong> it is strongly recommended to not
+ * omit this flag for any call to <code>saveLayer()</code> and
+ * <code>saveLayerAlpha()</code> variants. Not passing this flag generally
+ * triggers extremely poor performance with hardware accelerated rendering.
+ *
+ * @deprecated This flag results in poor performance and the same effect can be achieved with
+ * a single layer or multiple draw commands with different clips.
+ *
+ */
+ public static final int CLIP_TO_LAYER_SAVE_FLAG = 0x10;
+
+ /**
+ * Restore everything when restore() is called (standard save flags).
+ * <p class="note"><strong>Note:</strong> for performance reasons, it is
+ * strongly recommended to pass this - the complete set of flags - to any
+ * call to <code>saveLayer()</code> and <code>saveLayerAlpha()</code>
+ * variants.
+ *
+ * <p class="note"><strong>Note:</strong> all methods that accept this flag
+ * have flagless versions that are equivalent to passing this flag.
+ */
+ public static final int ALL_SAVE_FLAG = 0x1F;
+
+ /**
+ * Saves the current matrix and clip onto a private stack.
+ * <p>
+ * Subsequent calls to translate,scale,rotate,skew,concat or clipRect,
+ * clipPath will all operate as usual, but when the balancing call to
+ * restore() is made, those calls will be forgotten, and the settings that
+ * existed before the save() will be reinstated.
+ *
+ * @return The value to pass to restoreToCount() to balance this save()
+ */
+ public int save() {
+ return nSave(mNativeCanvasWrapper, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
+ }
+
+ /**
+ * Based on saveFlags, can save the current matrix and clip onto a private
+ * stack.
+ * <p class="note"><strong>Note:</strong> if possible, use the
+ * parameter-less save(). It is simpler and faster than individually
+ * disabling the saving of matrix or clip with this method.
+ * <p>
+ * Subsequent calls to translate,scale,rotate,skew,concat or clipRect,
+ * clipPath will all operate as usual, but when the balancing call to
+ * restore() is made, those calls will be forgotten, and the settings that
+ * existed before the save() will be reinstated.
+ *
+ * @deprecated Use {@link #save()} instead.
+ * @param saveFlags flag bits that specify which parts of the Canvas state
+ * to save/restore
+ * @return The value to pass to restoreToCount() to balance this save()
+ */
+ public int save(@Saveflags int saveFlags) {
+ return nSave(mNativeCanvasWrapper, saveFlags);
+ }
+
+ /**
+ * This behaves the same as save(), but in addition it allocates and
+ * redirects drawing to an offscreen bitmap.
+ * <p class="note"><strong>Note:</strong> this method is very expensive,
+ * incurring more than double rendering cost for contained content. Avoid
+ * using this method, especially if the bounds provided are large, or if
+ * the {@link #CLIP_TO_LAYER_SAVE_FLAG} is omitted from the
+ * {@code saveFlags} parameter. It is recommended to use a
+ * {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
+ * to apply an xfermode, color filter, or alpha, as it will perform much
+ * better than this method.
+ * <p>
+ * All drawing calls are directed to a newly allocated offscreen bitmap.
+ * Only when the balancing call to restore() is made, is that offscreen
+ * buffer drawn back to the current target of the Canvas (either the
+ * screen, it's target Bitmap, or the previous layer).
+ * <p>
+ * Attributes of the Paint - {@link Paint#getAlpha() alpha},
+ * {@link Paint#getXfermode() Xfermode}, and
+ * {@link Paint#getColorFilter() ColorFilter} are applied when the
+ * offscreen bitmap is drawn back when restore() is called.
+ *
+ * @deprecated Use {@link #saveLayer(RectF, Paint)} instead.
+ * @param bounds May be null. The maximum size the offscreen bitmap
+ * needs to be (in local coordinates)
+ * @param paint This is copied, and is applied to the offscreen when
+ * restore() is called.
+ * @param saveFlags see _SAVE_FLAG constants, generally {@link #ALL_SAVE_FLAG} is recommended
+ * for performance reasons.
+ * @return value to pass to restoreToCount() to balance this save()
+ */
+ public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags) {
+ if (bounds == null) {
+ bounds = new RectF(getClipBounds());
+ }
+ return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint, saveFlags);
+ }
+
+ /**
+ * This behaves the same as save(), but in addition it allocates and
+ * redirects drawing to an offscreen rendering target.
+ * <p class="note"><strong>Note:</strong> this method is very expensive,
+ * incurring more than double rendering cost for contained content. Avoid
+ * using this method when possible and instead use a
+ * {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
+ * to apply an xfermode, color filter, or alpha, as it will perform much
+ * better than this method.
+ * <p>
+ * All drawing calls are directed to a newly allocated offscreen rendering target.
+ * Only when the balancing call to restore() is made, is that offscreen
+ * buffer drawn back to the current target of the Canvas (which can potentially be a previous
+ * layer if these calls are nested).
+ * <p>
+ * Attributes of the Paint - {@link Paint#getAlpha() alpha},
+ * {@link Paint#getXfermode() Xfermode}, and
+ * {@link Paint#getColorFilter() ColorFilter} are applied when the
+ * offscreen rendering target is drawn back when restore() is called.
+ *
+ * @param bounds May be null. The maximum size the offscreen render target
+ * needs to be (in local coordinates)
+ * @param paint This is copied, and is applied to the offscreen when
+ * restore() is called.
+ * @return value to pass to restoreToCount() to balance this save()
+ */
+ public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) {
+ return saveLayer(bounds, paint, ALL_SAVE_FLAG);
+ }
+
+ /**
+ * Helper version of saveLayer() that takes 4 values rather than a RectF.
+ *
+ * @deprecated Use {@link #saveLayer(float, float, float, float, Paint)} instead.
+ */
+ public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
+ @Saveflags int saveFlags) {
+ return nSaveLayer(mNativeCanvasWrapper, left, top, right, bottom,
+ paint != null ? paint.getNativeInstance() : 0,
+ saveFlags);
+ }
+
+ /**
+ * Convenience for {@link #saveLayer(RectF, Paint)} that takes the four float coordinates of the
+ * bounds rectangle.
+ */
+ public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint) {
+ return saveLayer(left, top, right, bottom, paint, ALL_SAVE_FLAG);
+ }
+
+ /**
+ * This behaves the same as save(), but in addition it allocates and
+ * redirects drawing to an offscreen bitmap.
+ * <p class="note"><strong>Note:</strong> this method is very expensive,
+ * incurring more than double rendering cost for contained content. Avoid
+ * using this method, especially if the bounds provided are large, or if
+ * the {@link #CLIP_TO_LAYER_SAVE_FLAG} is omitted from the
+ * {@code saveFlags} parameter. It is recommended to use a
+ * {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
+ * to apply an xfermode, color filter, or alpha, as it will perform much
+ * better than this method.
+ * <p>
+ * All drawing calls are directed to a newly allocated offscreen bitmap.
+ * Only when the balancing call to restore() is made, is that offscreen
+ * buffer drawn back to the current target of the Canvas (either the
+ * screen, it's target Bitmap, or the previous layer).
+ * <p>
+ * The {@code alpha} parameter is applied when the offscreen bitmap is
+ * drawn back when restore() is called.
+ *
+ * @deprecated Use {@link #saveLayerAlpha(RectF, int)} instead.
+ * @param bounds The maximum size the offscreen bitmap needs to be
+ * (in local coordinates)
+ * @param alpha The alpha to apply to the offscreen when it is
+ drawn during restore()
+ * @param saveFlags see _SAVE_FLAG constants, generally {@link #ALL_SAVE_FLAG} is recommended
+ * for performance reasons.
+ * @return value to pass to restoreToCount() to balance this call
+ */
+ public int saveLayerAlpha(@Nullable RectF bounds, int alpha, @Saveflags int saveFlags) {
+ if (bounds == null) {
+ bounds = new RectF(getClipBounds());
+ }
+ return saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, alpha, saveFlags);
+ }
+
+ /**
+ * Convenience for {@link #saveLayer(RectF, Paint)} but instead of taking a entire Paint object
+ * it takes only the {@code alpha} parameter.
+ *
+ * @param bounds The maximum size the offscreen bitmap needs to be
+ * (in local coordinates)
+ * @param alpha The alpha to apply to the offscreen when it is
+ drawn during restore()
+ */
+ public int saveLayerAlpha(@Nullable RectF bounds, int alpha) {
+ return saveLayerAlpha(bounds, alpha, ALL_SAVE_FLAG);
+ }
+
+ /**
+ * Helper for saveLayerAlpha() that takes 4 values instead of a RectF.
+ *
+ * @deprecated Use {@link #saveLayerAlpha(float, float, float, float, int)} instead.
+ */
+ public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha,
+ @Saveflags int saveFlags) {
+ alpha = Math.min(255, Math.max(0, alpha));
+ return nSaveLayerAlpha(mNativeCanvasWrapper, left, top, right, bottom,
+ alpha, saveFlags);
+ }
+
+ /**
+ * Convenience for {@link #saveLayerAlpha(RectF, int)} that takes the four float coordinates of
+ * the bounds rectangle.
+ */
+ public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha) {
+ return saveLayerAlpha(left, top, right, bottom, alpha, ALL_SAVE_FLAG);
+ }
+
+ /**
+ * This call balances a previous call to save(), and is used to remove all
+ * modifications to the matrix/clip state since the last save call. It is
+ * an error to call restore() more times than save() was called.
+ */
+ public void restore() {
+ if (!nRestore(mNativeCanvasWrapper)
+ && (!sCompatibilityRestore || !isHardwareAccelerated())) {
+ throw new IllegalStateException("Underflow in restore - more restores than saves");
+ }
+ }
+
+ /**
+ * Returns the number of matrix/clip states on the Canvas' private stack.
+ * This will equal # save() calls - # restore() calls.
+ */
+ public int getSaveCount() {
+ return nGetSaveCount(mNativeCanvasWrapper);
+ }
+
+ /**
+ * Efficient way to pop any calls to save() that happened after the save
+ * count reached saveCount. It is an error for saveCount to be less than 1.
+ *
+ * Example:
+ * int count = canvas.save();
+ * ... // more calls potentially to save()
+ * canvas.restoreToCount(count);
+ * // now the canvas is back in the same state it was before the initial
+ * // call to save().
+ *
+ * @param saveCount The save level to restore to.
+ */
+ public void restoreToCount(int saveCount) {
+ if (saveCount < 1) {
+ if (!sCompatibilityRestore || !isHardwareAccelerated()) {
+ // do nothing and throw without restoring
+ throw new IllegalArgumentException(
+ "Underflow in restoreToCount - more restores than saves");
+ }
+ // compat behavior - restore as far as possible
+ saveCount = 1;
+ }
+ nRestoreToCount(mNativeCanvasWrapper, saveCount);
+ }
+
+ /**
+ * Preconcat the current matrix with the specified translation
+ *
+ * @param dx The distance to translate in X
+ * @param dy The distance to translate in Y
+ */
+ public void translate(float dx, float dy) {
+ if (dx == 0.0f && dy == 0.0f) return;
+ nTranslate(mNativeCanvasWrapper, dx, dy);
+ }
+
+ /**
+ * Preconcat the current matrix with the specified scale.
+ *
+ * @param sx The amount to scale in X
+ * @param sy The amount to scale in Y
+ */
+ public void scale(float sx, float sy) {
+ if (sx == 1.0f && sy == 1.0f) return;
+ nScale(mNativeCanvasWrapper, sx, sy);
+ }
+
+ /**
+ * Preconcat the current matrix with the specified scale.
+ *
+ * @param sx The amount to scale in X
+ * @param sy The amount to scale in Y
+ * @param px The x-coord for the pivot point (unchanged by the scale)
+ * @param py The y-coord for the pivot point (unchanged by the scale)
+ */
+ public final void scale(float sx, float sy, float px, float py) {
+ if (sx == 1.0f && sy == 1.0f) return;
+ translate(px, py);
+ scale(sx, sy);
+ translate(-px, -py);
+ }
+
+ /**
+ * Preconcat the current matrix with the specified rotation.
+ *
+ * @param degrees The amount to rotate, in degrees
+ */
+ public void rotate(float degrees) {
+ if (degrees == 0.0f) return;
+ nRotate(mNativeCanvasWrapper, degrees);
+ }
+
+ /**
+ * Preconcat the current matrix with the specified rotation.
+ *
+ * @param degrees The amount to rotate, in degrees
+ * @param px The x-coord for the pivot point (unchanged by the rotation)
+ * @param py The y-coord for the pivot point (unchanged by the rotation)
+ */
+ public final void rotate(float degrees, float px, float py) {
+ if (degrees == 0.0f) return;
+ translate(px, py);
+ rotate(degrees);
+ translate(-px, -py);
+ }
+
+ /**
+ * Preconcat the current matrix with the specified skew.
+ *
+ * @param sx The amount to skew in X
+ * @param sy The amount to skew in Y
+ */
+ public void skew(float sx, float sy) {
+ if (sx == 0.0f && sy == 0.0f) return;
+ nSkew(mNativeCanvasWrapper, sx, sy);
+ }
+
+ /**
+ * Preconcat the current matrix with the specified matrix. If the specified
+ * matrix is null, this method does nothing.
+ *
+ * @param matrix The matrix to preconcatenate with the current matrix
+ */
+ public void concat(@Nullable Matrix matrix) {
+ if (matrix != null) nConcat(mNativeCanvasWrapper, matrix.native_instance);
+ }
+
+ /**
+ * Completely replace the current matrix with the specified matrix. If the
+ * matrix parameter is null, then the current matrix is reset to identity.
+ *
+ * <strong>Note:</strong> it is recommended to use {@link #concat(Matrix)},
+ * {@link #scale(float, float)}, {@link #translate(float, float)} and
+ * {@link #rotate(float)} instead of this method.
+ *
+ * @param matrix The matrix to replace the current matrix with. If it is
+ * null, set the current matrix to identity.
+ *
+ * @see #concat(Matrix)
+ */
+ public void setMatrix(@Nullable Matrix matrix) {
+ nSetMatrix(mNativeCanvasWrapper,
+ matrix == null ? 0 : matrix.native_instance);
+ }
+
+ /**
+ * Return, in ctm, the current transformation matrix. This does not alter
+ * the matrix in the canvas, but just returns a copy of it.
+ *
+ * @deprecated {@link #isHardwareAccelerated() Hardware accelerated} canvases may have any
+ * matrix when passed to a View or Drawable, as it is implementation defined where in the
+ * hierarchy such canvases are created. It is recommended in such cases to either draw contents
+ * irrespective of the current matrix, or to track relevant transform state outside of the
+ * canvas.
+ */
+ @Deprecated
+ public void getMatrix(@NonNull Matrix ctm) {
+ nGetMatrix(mNativeCanvasWrapper, ctm.native_instance);
+ }
+
+ /**
+ * Return a new matrix with a copy of the canvas' current transformation
+ * matrix.
+ *
+ * @deprecated {@link #isHardwareAccelerated() Hardware accelerated} canvases may have any
+ * matrix when passed to a View or Drawable, as it is implementation defined where in the
+ * hierarchy such canvases are created. It is recommended in such cases to either draw contents
+ * irrespective of the current matrix, or to track relevant transform state outside of the
+ * canvas.
+ */
+ @Deprecated
+ public final @NonNull Matrix getMatrix() {
+ Matrix m = new Matrix();
+ //noinspection deprecation
+ getMatrix(m);
+ return m;
+ }
+
+ /**
+ * Modify the current clip with the specified rectangle.
+ *
+ * @param rect The rect to intersect with the current clip
+ * @param op How the clip is modified
+ * @return true if the resulting clip is non-empty
+ *
+ * @deprecated Region.Op values other than {@link Region.Op#INTERSECT} and
+ * {@link Region.Op#DIFFERENCE} have the ability to expand the clip. The canvas clipping APIs
+ * are intended to only expand the clip as a result of a restore operation. This enables a view
+ * parent to clip a canvas to clearly define the maximal drawing area of its children. The
+ * recommended alternative calls are {@link #clipRect(RectF)} and {@link #clipOutRect(RectF)};
+ */
+ @Deprecated
+ public boolean clipRect(@NonNull RectF rect, @NonNull Region.Op op) {
+ return nClipRect(mNativeCanvasWrapper, rect.left, rect.top, rect.right, rect.bottom,
+ op.nativeInt);
+ }
+
+ /**
+ * Modify the current clip with the specified rectangle, which is
+ * expressed in local coordinates.
+ *
+ * @param rect The rectangle to intersect with the current clip.
+ * @param op How the clip is modified
+ * @return true if the resulting clip is non-empty
+ *
+ * @deprecated Region.Op values other than {@link Region.Op#INTERSECT} and
+ * {@link Region.Op#DIFFERENCE} have the ability to expand the clip. The canvas clipping APIs
+ * are intended to only expand the clip as a result of a restore operation. This enables a view
+ * parent to clip a canvas to clearly define the maximal drawing area of its children. The
+ * recommended alternative calls are {@link #clipRect(Rect)} and {@link #clipOutRect(Rect)};
+ */
+ @Deprecated
+ public boolean clipRect(@NonNull Rect rect, @NonNull Region.Op op) {
+ return nClipRect(mNativeCanvasWrapper, rect.left, rect.top, rect.right, rect.bottom,
+ op.nativeInt);
+ }
+
+ /**
+ * Intersect the current clip with the specified rectangle, which is
+ * expressed in local coordinates.
+ *
+ * @param rect The rectangle to intersect with the current clip.
+ * @return true if the resulting clip is non-empty
+ */
+ public boolean clipRect(@NonNull RectF rect) {
+ return nClipRect(mNativeCanvasWrapper, rect.left, rect.top, rect.right, rect.bottom,
+ Region.Op.INTERSECT.nativeInt);
+ }
+
+ /**
+ * Set the clip to the difference of the current clip and the specified rectangle, which is
+ * expressed in local coordinates.
+ *
+ * @param rect The rectangle to perform a difference op with the current clip.
+ * @return true if the resulting clip is non-empty
+ */
+ public boolean clipOutRect(@NonNull RectF rect) {
+ return nClipRect(mNativeCanvasWrapper, rect.left, rect.top, rect.right, rect.bottom,
+ Region.Op.DIFFERENCE.nativeInt);
+ }
+
+ /**
+ * Intersect the current clip with the specified rectangle, which is
+ * expressed in local coordinates.
+ *
+ * @param rect The rectangle to intersect with the current clip.
+ * @return true if the resulting clip is non-empty
+ */
+ public boolean clipRect(@NonNull Rect rect) {
+ return nClipRect(mNativeCanvasWrapper, rect.left, rect.top, rect.right, rect.bottom,
+ Region.Op.INTERSECT.nativeInt);
+ }
+
+ /**
+ * Set the clip to the difference of the current clip and the specified rectangle, which is
+ * expressed in local coordinates.
+ *
+ * @param rect The rectangle to perform a difference op with the current clip.
+ * @return true if the resulting clip is non-empty
+ */
+ public boolean clipOutRect(@NonNull Rect rect) {
+ return nClipRect(mNativeCanvasWrapper, rect.left, rect.top, rect.right, rect.bottom,
+ Region.Op.DIFFERENCE.nativeInt);
+ }
+
+ /**
+ * Modify the current clip with the specified rectangle, which is
+ * expressed in local coordinates.
+ *
+ * @param left The left side of the rectangle to intersect with the
+ * current clip
+ * @param top The top of the rectangle to intersect with the current
+ * clip
+ * @param right The right side of the rectangle to intersect with the
+ * current clip
+ * @param bottom The bottom of the rectangle to intersect with the current
+ * clip
+ * @param op How the clip is modified
+ * @return true if the resulting clip is non-empty
+ *
+ * @deprecated Region.Op values other than {@link Region.Op#INTERSECT} and
+ * {@link Region.Op#DIFFERENCE} have the ability to expand the clip. The canvas clipping APIs
+ * are intended to only expand the clip as a result of a restore operation. This enables a view
+ * parent to clip a canvas to clearly define the maximal drawing area of its children. The
+ * recommended alternative calls are {@link #clipRect(float,float,float,float)} and
+ * {@link #clipOutRect(float,float,float,float)};
+ */
+ @Deprecated
+ public boolean clipRect(float left, float top, float right, float bottom,
+ @NonNull Region.Op op) {
+ return nClipRect(mNativeCanvasWrapper, left, top, right, bottom, op.nativeInt);
+ }
+
+ /**
+ * Intersect the current clip with the specified rectangle, which is
+ * expressed in local coordinates.
+ *
+ * @param left The left side of the rectangle to intersect with the
+ * current clip
+ * @param top The top of the rectangle to intersect with the current clip
+ * @param right The right side of the rectangle to intersect with the
+ * current clip
+ * @param bottom The bottom of the rectangle to intersect with the current
+ * clip
+ * @return true if the resulting clip is non-empty
+ */
+ public boolean clipRect(float left, float top, float right, float bottom) {
+ return nClipRect(mNativeCanvasWrapper, left, top, right, bottom,
+ Region.Op.INTERSECT.nativeInt);
+ }
+
+ /**
+ * Set the clip to the difference of the current clip and the specified rectangle, which is
+ * expressed in local coordinates.
+ *
+ * @param left The left side of the rectangle used in the difference operation
+ * @param top The top of the rectangle used in the difference operation
+ * @param right The right side of the rectangle used in the difference operation
+ * @param bottom The bottom of the rectangle used in the difference operation
+ * @return true if the resulting clip is non-empty
+ */
+ public boolean clipOutRect(float left, float top, float right, float bottom) {
+ return nClipRect(mNativeCanvasWrapper, left, top, right, bottom,
+ Region.Op.DIFFERENCE.nativeInt);
+ }
+
+ /**
+ * Intersect the current clip with the specified rectangle, which is
+ * expressed in local coordinates.
+ *
+ * @param left The left side of the rectangle to intersect with the
+ * current clip
+ * @param top The top of the rectangle to intersect with the current clip
+ * @param right The right side of the rectangle to intersect with the
+ * current clip
+ * @param bottom The bottom of the rectangle to intersect with the current
+ * clip
+ * @return true if the resulting clip is non-empty
+ */
+ public boolean clipRect(int left, int top, int right, int bottom) {
+ return nClipRect(mNativeCanvasWrapper, left, top, right, bottom,
+ Region.Op.INTERSECT.nativeInt);
+ }
+
+ /**
+ * Set the clip to the difference of the current clip and the specified rectangle, which is
+ * expressed in local coordinates.
+ *
+ * @param left The left side of the rectangle used in the difference operation
+ * @param top The top of the rectangle used in the difference operation
+ * @param right The right side of the rectangle used in the difference operation
+ * @param bottom The bottom of the rectangle used in the difference operation
+ * @return true if the resulting clip is non-empty
+ */
+ public boolean clipOutRect(int left, int top, int right, int bottom) {
+ return nClipRect(mNativeCanvasWrapper, left, top, right, bottom,
+ Region.Op.DIFFERENCE.nativeInt);
+ }
+
+ /**
+ * Modify the current clip with the specified path.
+ *
+ * @param path The path to operate on the current clip
+ * @param op How the clip is modified
+ * @return true if the resulting is non-empty
+ *
+ * @deprecated Region.Op values other than {@link Region.Op#INTERSECT} and
+ * {@link Region.Op#DIFFERENCE} have the ability to expand the clip. The canvas clipping APIs
+ * are intended to only expand the clip as a result of a restore operation. This enables a view
+ * parent to clip a canvas to clearly define the maximal drawing area of its children. The
+ * recommended alternative calls are {@link #clipPath(Path)} and
+ * {@link #clipOutPath(Path)};
+ */
+ @Deprecated
+ public boolean clipPath(@NonNull Path path, @NonNull Region.Op op) {
+ return nClipPath(mNativeCanvasWrapper, path.readOnlyNI(), op.nativeInt);
+ }
+
+ /**
+ * Intersect the current clip with the specified path.
+ *
+ * @param path The path to intersect with the current clip
+ * @return true if the resulting clip is non-empty
+ */
+ public boolean clipPath(@NonNull Path path) {
+ return clipPath(path, Region.Op.INTERSECT);
+ }
+
+ /**
+ * Set the clip to the difference of the current clip and the specified path.
+ *
+ * @param path The path used in the difference operation
+ * @return true if the resulting clip is non-empty
+ */
+ public boolean clipOutPath(@NonNull Path path) {
+ return clipPath(path, Region.Op.DIFFERENCE);
+ }
+
+ /**
+ * Modify the current clip with the specified region. Note that unlike
+ * clipRect() and clipPath() which transform their arguments by the
+ * current matrix, clipRegion() assumes its argument is already in the
+ * coordinate system of the current layer's bitmap, and so not
+ * transformation is performed.
+ *
+ * @param region The region to operate on the current clip, based on op
+ * @param op How the clip is modified
+ * @return true if the resulting is non-empty
+ *
+ * @removed
+ * @deprecated Unlike all other clip calls this API does not respect the
+ * current matrix. Use {@link #clipRect(Rect)} as an alternative.
+ */
+ @Deprecated
+ public boolean clipRegion(@NonNull Region region, @NonNull Region.Op op) {
+ return false;
+ }
+
+ /**
+ * Intersect the current clip with the specified region. Note that unlike
+ * clipRect() and clipPath() which transform their arguments by the
+ * current matrix, clipRegion() assumes its argument is already in the
+ * coordinate system of the current layer's bitmap, and so not
+ * transformation is performed.
+ *
+ * @param region The region to operate on the current clip, based on op
+ * @return true if the resulting is non-empty
+ *
+ * @removed
+ * @deprecated Unlike all other clip calls this API does not respect the
+ * current matrix. Use {@link #clipRect(Rect)} as an alternative.
+ */
+ @Deprecated
+ public boolean clipRegion(@NonNull Region region) {
+ return false;
+ }
+
+ public @Nullable DrawFilter getDrawFilter() {
+ return mDrawFilter;
+ }
+
+ public void setDrawFilter(@Nullable DrawFilter filter) {
+ long nativeFilter = 0;
+ if (filter != null) {
+ nativeFilter = filter.mNativeInt;
+ }
+ mDrawFilter = filter;
+ nSetDrawFilter(mNativeCanvasWrapper, nativeFilter);
+ }
+
+ /**
+ * Constant values used as parameters to {@code quickReject()} calls. These values
+ * specify how much space around the shape should be accounted for, depending on whether
+ * the shaped area is antialiased or not.
+ *
+ * @see #quickReject(float, float, float, float, EdgeType)
+ * @see #quickReject(Path, EdgeType)
+ * @see #quickReject(RectF, EdgeType)
+ */
+ public enum EdgeType {
+
+ /**
+ * Black-and-White: Treat edges by just rounding to nearest pixel boundary
+ */
+ BW(0), //!< treat edges by just rounding to nearest pixel boundary
+
+ /**
+ * Antialiased: Treat edges by rounding-out, since they may be antialiased
+ */
+ AA(1);
+
+ EdgeType(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+
+ /**
+ * @hide
+ */
+ public final int nativeInt;
+ }
+
+ /**
+ * Return true if the specified rectangle, after being transformed by the
+ * current matrix, would lie completely outside of the current clip. Call
+ * this to check if an area you intend to draw into is clipped out (and
+ * therefore you can skip making the draw calls).
+ *
+ * @param rect the rect to compare with the current clip
+ * @param type {@link Canvas.EdgeType#AA} if the path should be considered antialiased,
+ * since that means it may affect a larger area (more pixels) than
+ * non-antialiased ({@link Canvas.EdgeType#BW}).
+ * @return true if the rect (transformed by the canvas' matrix)
+ * does not intersect with the canvas' clip
+ */
+ public boolean quickReject(@NonNull RectF rect, @NonNull EdgeType type) {
+ return nQuickReject(mNativeCanvasWrapper,
+ rect.left, rect.top, rect.right, rect.bottom);
+ }
+
+ /**
+ * Return true if the specified path, after being transformed by the
+ * current matrix, would lie completely outside of the current clip. Call
+ * this to check if an area you intend to draw into is clipped out (and
+ * therefore you can skip making the draw calls). Note: for speed it may
+ * return false even if the path itself might not intersect the clip
+ * (i.e. the bounds of the path intersects, but the path does not).
+ *
+ * @param path The path to compare with the current clip
+ * @param type {@link Canvas.EdgeType#AA} if the path should be considered antialiased,
+ * since that means it may affect a larger area (more pixels) than
+ * non-antialiased ({@link Canvas.EdgeType#BW}).
+ * @return true if the path (transformed by the canvas' matrix)
+ * does not intersect with the canvas' clip
+ */
+ public boolean quickReject(@NonNull Path path, @NonNull EdgeType type) {
+ return nQuickReject(mNativeCanvasWrapper, path.readOnlyNI());
+ }
+
+ /**
+ * Return true if the specified rectangle, after being transformed by the
+ * current matrix, would lie completely outside of the current clip. Call
+ * this to check if an area you intend to draw into is clipped out (and
+ * therefore you can skip making the draw calls).
+ *
+ * @param left The left side of the rectangle to compare with the
+ * current clip
+ * @param top The top of the rectangle to compare with the current
+ * clip
+ * @param right The right side of the rectangle to compare with the
+ * current clip
+ * @param bottom The bottom of the rectangle to compare with the
+ * current clip
+ * @param type {@link Canvas.EdgeType#AA} if the path should be considered antialiased,
+ * since that means it may affect a larger area (more pixels) than
+ * non-antialiased ({@link Canvas.EdgeType#BW}).
+ * @return true if the rect (transformed by the canvas' matrix)
+ * does not intersect with the canvas' clip
+ */
+ public boolean quickReject(float left, float top, float right, float bottom,
+ @NonNull EdgeType type) {
+ return nQuickReject(mNativeCanvasWrapper, left, top, right, bottom);
+ }
+
+ /**
+ * Return the bounds of the current clip (in local coordinates) in the
+ * bounds parameter, and return true if it is non-empty. This can be useful
+ * in a way similar to quickReject, in that it tells you that drawing
+ * outside of these bounds will be clipped out.
+ *
+ * @param bounds Return the clip bounds here. If it is null, ignore it but
+ * still return true if the current clip is non-empty.
+ * @return true if the current clip is non-empty.
+ */
+ public boolean getClipBounds(@Nullable Rect bounds) {
+ return nGetClipBounds(mNativeCanvasWrapper, bounds);
+ }
+
+ /**
+ * Retrieve the bounds of the current clip (in local coordinates).
+ *
+ * @return the clip bounds, or [0, 0, 0, 0] if the clip is empty.
+ */
+ public final @NonNull Rect getClipBounds() {
+ Rect r = new Rect();
+ getClipBounds(r);
+ return r;
+ }
+
+ /**
+ * Save the canvas state, draw the picture, and restore the canvas state.
+ * This differs from picture.draw(canvas), which does not perform any
+ * save/restore.
+ *
+ * <p>
+ * <strong>Note:</strong> This forces the picture to internally call
+ * {@link Picture#endRecording} in order to prepare for playback.
+ *
+ * @param picture The picture to be drawn
+ */
+ public void drawPicture(@NonNull Picture picture) {
+ picture.endRecording();
+ int restoreCount = save();
+ picture.draw(this);
+ restoreToCount(restoreCount);
+ }
+
+ /**
+ * Draw the picture, stretched to fit into the dst rectangle.
+ */
+ public void drawPicture(@NonNull Picture picture, @NonNull RectF dst) {
+ save();
+ translate(dst.left, dst.top);
+ if (picture.getWidth() > 0 && picture.getHeight() > 0) {
+ scale(dst.width() / picture.getWidth(), dst.height() / picture.getHeight());
+ }
+ drawPicture(picture);
+ restore();
+ }
+
+ /**
+ * Draw the picture, stretched to fit into the dst rectangle.
+ */
+ public void drawPicture(@NonNull Picture picture, @NonNull Rect dst) {
+ save();
+ translate(dst.left, dst.top);
+ if (picture.getWidth() > 0 && picture.getHeight() > 0) {
+ scale((float) dst.width() / picture.getWidth(),
+ (float) dst.height() / picture.getHeight());
+ }
+ drawPicture(picture);
+ restore();
+ }
+
+ public enum VertexMode {
+ TRIANGLES(0),
+ TRIANGLE_STRIP(1),
+ TRIANGLE_FAN(2);
+
+ VertexMode(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+
+ /**
+ * @hide
+ */
+ public final int nativeInt;
+ }
+
+ /**
+ * Releases the resources associated with this canvas.
+ *
+ * @hide
+ */
+ public void release() {
+ mNativeCanvasWrapper = 0;
+ if (mFinalizer != null) {
+ mFinalizer.run();
+ mFinalizer = null;
+ }
+ }
+
+ /**
+ * Free up as much memory as possible from private caches (e.g. fonts, images)
+ *
+ * @hide
+ */
+ public static void freeCaches() {
+ nFreeCaches();
+ }
+
+ /**
+ * Free up text layout caches
+ *
+ * @hide
+ */
+ public static void freeTextLayoutCaches() {
+ nFreeTextLayoutCaches();
+ }
+
+ private static native void nFreeCaches();
+ private static native void nFreeTextLayoutCaches();
+ private static native long nInitRaster(Bitmap bitmap);
+ private static native long nGetNativeFinalizer();
+
+ // ---------------- @FastNative -------------------
+
+ @FastNative
+ private static native void nSetBitmap(long canvasHandle, Bitmap bitmap);
+
+ @FastNative
+ private static native boolean nGetClipBounds(long nativeCanvas, Rect bounds);
+
+ // ---------------- @CriticalNative -------------------
+
+ @CriticalNative
+ private static native boolean nIsOpaque(long canvasHandle);
+ @CriticalNative
+ private static native int nGetWidth(long canvasHandle);
+ @CriticalNative
+ private static native int nGetHeight(long canvasHandle);
+
+ @CriticalNative
+ private static native int nSave(long canvasHandle, int saveFlags);
+ @CriticalNative
+ private static native int nSaveLayer(long nativeCanvas, float l, float t, float r, float b,
+ long nativePaint, int layerFlags);
+ @CriticalNative
+ private static native int nSaveLayerAlpha(long nativeCanvas, float l, float t, float r, float b,
+ int alpha, int layerFlags);
+ @CriticalNative
+ private static native boolean nRestore(long canvasHandle);
+ @CriticalNative
+ private static native void nRestoreToCount(long canvasHandle, int saveCount);
+ @CriticalNative
+ private static native int nGetSaveCount(long canvasHandle);
+
+ @CriticalNative
+ private static native void nTranslate(long canvasHandle, float dx, float dy);
+ @CriticalNative
+ private static native void nScale(long canvasHandle, float sx, float sy);
+ @CriticalNative
+ private static native void nRotate(long canvasHandle, float degrees);
+ @CriticalNative
+ private static native void nSkew(long canvasHandle, float sx, float sy);
+ @CriticalNative
+ private static native void nConcat(long nativeCanvas, long nativeMatrix);
+ @CriticalNative
+ private static native void nSetMatrix(long nativeCanvas, long nativeMatrix);
+ @CriticalNative
+ private static native boolean nClipRect(long nativeCanvas,
+ float left, float top, float right, float bottom, int regionOp);
+ @CriticalNative
+ private static native boolean nClipPath(long nativeCanvas, long nativePath, int regionOp);
+ @CriticalNative
+ private static native void nSetDrawFilter(long nativeCanvas, long nativeFilter);
+ @CriticalNative
+ private static native void nGetMatrix(long nativeCanvas, long nativeMatrix);
+ @CriticalNative
+ private static native boolean nQuickReject(long nativeCanvas, long nativePath);
+ @CriticalNative
+ private static native boolean nQuickReject(long nativeCanvas, float left, float top,
+ float right, float bottom);
+
+
+ // ---------------- Draw Methods -------------------
+
+ /**
+ * <p>
+ * Draw the specified arc, which will be scaled to fit inside the specified oval.
+ * </p>
+ * <p>
+ * If the start angle is negative or >= 360, the start angle is treated as start angle modulo
+ * 360.
+ * </p>
+ * <p>
+ * If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs
+ * slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is
+ * negative, the sweep angle is treated as sweep angle modulo 360
+ * </p>
+ * <p>
+ * The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0
+ * degrees (3 o'clock on a watch.)
+ * </p>
+ *
+ * @param oval The bounds of oval used to define the shape and size of the arc
+ * @param startAngle Starting angle (in degrees) where the arc begins
+ * @param sweepAngle Sweep angle (in degrees) measured clockwise
+ * @param useCenter If true, include the center of the oval in the arc, and close it if it is
+ * being stroked. This will draw a wedge
+ * @param paint The paint used to draw the arc
+ */
+ public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
+ @NonNull Paint paint) {
+ super.drawArc(oval, startAngle, sweepAngle, useCenter, paint);
+ }
+
+ /**
+ * <p>
+ * Draw the specified arc, which will be scaled to fit inside the specified oval.
+ * </p>
+ * <p>
+ * If the start angle is negative or >= 360, the start angle is treated as start angle modulo
+ * 360.
+ * </p>
+ * <p>
+ * If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs
+ * slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is
+ * negative, the sweep angle is treated as sweep angle modulo 360
+ * </p>
+ * <p>
+ * The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0
+ * degrees (3 o'clock on a watch.)
+ * </p>
+ *
+ * @param startAngle Starting angle (in degrees) where the arc begins
+ * @param sweepAngle Sweep angle (in degrees) measured clockwise
+ * @param useCenter If true, include the center of the oval in the arc, and close it if it is
+ * being stroked. This will draw a wedge
+ * @param paint The paint used to draw the arc
+ */
+ public void drawArc(float left, float top, float right, float bottom, float startAngle,
+ float sweepAngle, boolean useCenter, @NonNull Paint paint) {
+ super.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint);
+ }
+
+ /**
+ * Fill the entire canvas' bitmap (restricted to the current clip) with the specified ARGB
+ * color, using srcover porterduff mode.
+ *
+ * @param a alpha component (0..255) of the color to draw onto the canvas
+ * @param r red component (0..255) of the color to draw onto the canvas
+ * @param g green component (0..255) of the color to draw onto the canvas
+ * @param b blue component (0..255) of the color to draw onto the canvas
+ */
+ public void drawARGB(int a, int r, int g, int b) {
+ super.drawARGB(a, r, g, b);
+ }
+
+ /**
+ * Draw the specified bitmap, with its top/left corner at (x,y), using the specified paint,
+ * transformed by the current matrix.
+ * <p>
+ * Note: if the paint contains a maskfilter that generates a mask which extends beyond the
+ * bitmap's original width/height (e.g. BlurMaskFilter), then the bitmap will be drawn as if it
+ * were in a Shader with CLAMP mode. Thus the color outside of the original width/height will be
+ * the edge color replicated.
+ * <p>
+ * If the bitmap and canvas have different densities, this function will take care of
+ * automatically scaling the bitmap to draw at the same density as the canvas.
+ *
+ * @param bitmap The bitmap to be drawn
+ * @param left The position of the left side of the bitmap being drawn
+ * @param top The position of the top side of the bitmap being drawn
+ * @param paint The paint used to draw the bitmap (may be null)
+ */
+ public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint) {
+ super.drawBitmap(bitmap, left, top, paint);
+ }
+
+ /**
+ * Draw the specified bitmap, scaling/translating automatically to fill the destination
+ * rectangle. If the source rectangle is not null, it specifies the subset of the bitmap to
+ * draw.
+ * <p>
+ * Note: if the paint contains a maskfilter that generates a mask which extends beyond the
+ * bitmap's original width/height (e.g. BlurMaskFilter), then the bitmap will be drawn as if it
+ * were in a Shader with CLAMP mode. Thus the color outside of the original width/height will be
+ * the edge color replicated.
+ * <p>
+ * This function <em>ignores the density associated with the bitmap</em>. This is because the
+ * source and destination rectangle coordinate spaces are in their respective densities, so must
+ * already have the appropriate scaling factor applied.
+ *
+ * @param bitmap The bitmap to be drawn
+ * @param src May be null. The subset of the bitmap to be drawn
+ * @param dst The rectangle that the bitmap will be scaled/translated to fit into
+ * @param paint May be null. The paint used to draw the bitmap
+ */
+ public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst,
+ @Nullable Paint paint) {
+ super.drawBitmap(bitmap, src, dst, paint);
+ }
+
+ /**
+ * Draw the specified bitmap, scaling/translating automatically to fill the destination
+ * rectangle. If the source rectangle is not null, it specifies the subset of the bitmap to
+ * draw.
+ * <p>
+ * Note: if the paint contains a maskfilter that generates a mask which extends beyond the
+ * bitmap's original width/height (e.g. BlurMaskFilter), then the bitmap will be drawn as if it
+ * were in a Shader with CLAMP mode. Thus the color outside of the original width/height will be
+ * the edge color replicated.
+ * <p>
+ * This function <em>ignores the density associated with the bitmap</em>. This is because the
+ * source and destination rectangle coordinate spaces are in their respective densities, so must
+ * already have the appropriate scaling factor applied.
+ *
+ * @param bitmap The bitmap to be drawn
+ * @param src May be null. The subset of the bitmap to be drawn
+ * @param dst The rectangle that the bitmap will be scaled/translated to fit into
+ * @param paint May be null. The paint used to draw the bitmap
+ */
+ public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,
+ @Nullable Paint paint) {
+ super.drawBitmap(bitmap, src, dst, paint);
+ }
+
+ /**
+ * Treat the specified array of colors as a bitmap, and draw it. This gives the same result as
+ * first creating a bitmap from the array, and then drawing it, but this method avoids
+ * explicitly creating a bitmap object which can be more efficient if the colors are changing
+ * often.
+ *
+ * @param colors Array of colors representing the pixels of the bitmap
+ * @param offset Offset into the array of colors for the first pixel
+ * @param stride The number of colors in the array between rows (must be >= width or <= -width).
+ * @param x The X coordinate for where to draw the bitmap
+ * @param y The Y coordinate for where to draw the bitmap
+ * @param width The width of the bitmap
+ * @param height The height of the bitmap
+ * @param hasAlpha True if the alpha channel of the colors contains valid values. If false, the
+ * alpha byte is ignored (assumed to be 0xFF for every pixel).
+ * @param paint May be null. The paint used to draw the bitmap
+ * @deprecated Usage with a {@link #isHardwareAccelerated() hardware accelerated} canvas
+ * requires an internal copy of color buffer contents every time this method is
+ * called. Using a Bitmap avoids this copy, and allows the application to more
+ * explicitly control the lifetime and copies of pixel data.
+ */
+ @Deprecated
+ public void drawBitmap(@NonNull int[] colors, int offset, int stride, float x, float y,
+ int width, int height, boolean hasAlpha, @Nullable Paint paint) {
+ super.drawBitmap(colors, offset, stride, x, y, width, height, hasAlpha, paint);
+ }
+
+ /**
+ * Legacy version of drawBitmap(int[] colors, ...) that took ints for x,y
+ *
+ * @deprecated Usage with a {@link #isHardwareAccelerated() hardware accelerated} canvas
+ * requires an internal copy of color buffer contents every time this method is
+ * called. Using a Bitmap avoids this copy, and allows the application to more
+ * explicitly control the lifetime and copies of pixel data.
+ */
+ @Deprecated
+ public void drawBitmap(@NonNull int[] colors, int offset, int stride, int x, int y,
+ int width, int height, boolean hasAlpha, @Nullable Paint paint) {
+ super.drawBitmap(colors, offset, stride, x, y, width, height, hasAlpha, paint);
+ }
+
+ /**
+ * Draw the bitmap using the specified matrix.
+ *
+ * @param bitmap The bitmap to draw
+ * @param matrix The matrix used to transform the bitmap when it is drawn
+ * @param paint May be null. The paint used to draw the bitmap
+ */
+ public void drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, @Nullable Paint paint) {
+ super.drawBitmap(bitmap, matrix, paint);
+ }
+
+ /**
+ * Draw the bitmap through the mesh, where mesh vertices are evenly distributed across the
+ * bitmap. There are meshWidth+1 vertices across, and meshHeight+1 vertices down. The verts
+ * array is accessed in row-major order, so that the first meshWidth+1 vertices are distributed
+ * across the top of the bitmap from left to right. A more general version of this method is
+ * drawVertices().
+ *
+ * @param bitmap The bitmap to draw using the mesh
+ * @param meshWidth The number of columns in the mesh. Nothing is drawn if this is 0
+ * @param meshHeight The number of rows in the mesh. Nothing is drawn if this is 0
+ * @param verts Array of x,y pairs, specifying where the mesh should be drawn. There must be at
+ * least (meshWidth+1) * (meshHeight+1) * 2 + vertOffset values in the array
+ * @param vertOffset Number of verts elements to skip before drawing
+ * @param colors May be null. Specifies a color at each vertex, which is interpolated across the
+ * cell, and whose values are multiplied by the corresponding bitmap colors. If not
+ * null, there must be at least (meshWidth+1) * (meshHeight+1) + colorOffset values
+ * in the array.
+ * @param colorOffset Number of color elements to skip before drawing
+ * @param paint May be null. The paint used to draw the bitmap
+ */
+ public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight,
+ @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
+ @Nullable Paint paint) {
+ super.drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset,
+ paint);
+ }
+
+ /**
+ * Draw the specified circle using the specified paint. If radius is <= 0, then nothing will be
+ * drawn. The circle will be filled or framed based on the Style in the paint.
+ *
+ * @param cx The x-coordinate of the center of the cirle to be drawn
+ * @param cy The y-coordinate of the center of the cirle to be drawn
+ * @param radius The radius of the cirle to be drawn
+ * @param paint The paint used to draw the circle
+ */
+ public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) {
+ super.drawCircle(cx, cy, radius, paint);
+ }
+
+ /**
+ * Fill the entire canvas' bitmap (restricted to the current clip) with the specified color,
+ * using srcover porterduff mode.
+ *
+ * @param color the color to draw onto the canvas
+ */
+ public void drawColor(@ColorInt int color) {
+ super.drawColor(color);
+ }
+
+ /**
+ * Fill the entire canvas' bitmap (restricted to the current clip) with the specified color and
+ * porter-duff xfermode.
+ *
+ * @param color the color to draw with
+ * @param mode the porter-duff mode to apply to the color
+ */
+ public void drawColor(@ColorInt int color, @NonNull PorterDuff.Mode mode) {
+ super.drawColor(color, mode);
+ }
+
+ /**
+ * Draw a line segment with the specified start and stop x,y coordinates, using the specified
+ * paint.
+ * <p>
+ * Note that since a line is always "framed", the Style is ignored in the paint.
+ * </p>
+ * <p>
+ * Degenerate lines (length is 0) will not be drawn.
+ * </p>
+ *
+ * @param startX The x-coordinate of the start point of the line
+ * @param startY The y-coordinate of the start point of the line
+ * @param paint The paint used to draw the line
+ */
+ public void drawLine(float startX, float startY, float stopX, float stopY,
+ @NonNull Paint paint) {
+ super.drawLine(startX, startY, stopX, stopY, paint);
+ }
+
+ /**
+ * Draw a series of lines. Each line is taken from 4 consecutive values in the pts array. Thus
+ * to draw 1 line, the array must contain at least 4 values. This is logically the same as
+ * drawing the array as follows: drawLine(pts[0], pts[1], pts[2], pts[3]) followed by
+ * drawLine(pts[4], pts[5], pts[6], pts[7]) and so on.
+ *
+ * @param pts Array of points to draw [x0 y0 x1 y1 x2 y2 ...]
+ * @param offset Number of values in the array to skip before drawing.
+ * @param count The number of values in the array to process, after skipping "offset" of them.
+ * Since each line uses 4 values, the number of "lines" that are drawn is really
+ * (count >> 2).
+ * @param paint The paint used to draw the points
+ */
+ public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count,
+ @NonNull Paint paint) {
+ super.drawLines(pts, offset, count, paint);
+ }
+
+ public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint) {
+ super.drawLines(pts, paint);
+ }
+
+ /**
+ * Draw the specified oval using the specified paint. The oval will be filled or framed based on
+ * the Style in the paint.
+ *
+ * @param oval The rectangle bounds of the oval to be drawn
+ */
+ public void drawOval(@NonNull RectF oval, @NonNull Paint paint) {
+ super.drawOval(oval, paint);
+ }
+
+ /**
+ * Draw the specified oval using the specified paint. The oval will be filled or framed based on
+ * the Style in the paint.
+ */
+ public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint) {
+ super.drawOval(left, top, right, bottom, paint);
+ }
+
+ /**
+ * Fill the entire canvas' bitmap (restricted to the current clip) with the specified paint.
+ * This is equivalent (but faster) to drawing an infinitely large rectangle with the specified
+ * paint.
+ *
+ * @param paint The paint used to draw onto the canvas
+ */
+ public void drawPaint(@NonNull Paint paint) {
+ super.drawPaint(paint);
+ }
+
+ /**
+ * Draws the specified bitmap as an N-patch (most often, a 9-patches.)
+ *
+ * @param patch The ninepatch object to render
+ * @param dst The destination rectangle.
+ * @param paint The paint to draw the bitmap with. may be null
+ * @hide
+ */
+ public void drawPatch(@NonNull NinePatch patch, @NonNull Rect dst, @Nullable Paint paint) {
+ super.drawPatch(patch, dst, paint);
+ }
+
+ /**
+ * Draws the specified bitmap as an N-patch (most often, a 9-patches.)
+ *
+ * @param patch The ninepatch object to render
+ * @param dst The destination rectangle.
+ * @param paint The paint to draw the bitmap with. may be null
+ * @hide
+ */
+ public void drawPatch(@NonNull NinePatch patch, @NonNull RectF dst, @Nullable Paint paint) {
+ super.drawPatch(patch, dst, paint);
+ }
+
+ /**
+ * Draw the specified path using the specified paint. The path will be filled or framed based on
+ * the Style in the paint.
+ *
+ * @param path The path to be drawn
+ * @param paint The paint used to draw the path
+ */
+ public void drawPath(@NonNull Path path, @NonNull Paint paint) {
+ super.drawPath(path, paint);
+ }
+
+ /**
+ * Helper for drawPoints() for drawing a single point.
+ */
+ public void drawPoint(float x, float y, @NonNull Paint paint) {
+ super.drawPoint(x, y, paint);
+ }
+
+ /**
+ * Draw a series of points. Each point is centered at the coordinate specified by pts[], and its
+ * diameter is specified by the paint's stroke width (as transformed by the canvas' CTM), with
+ * special treatment for a stroke width of 0, which always draws exactly 1 pixel (or at most 4
+ * if antialiasing is enabled). The shape of the point is controlled by the paint's Cap type.
+ * The shape is a square, unless the cap type is Round, in which case the shape is a circle.
+ *
+ * @param pts Array of points to draw [x0 y0 x1 y1 x2 y2 ...]
+ * @param offset Number of values to skip before starting to draw.
+ * @param count The number of values to process, after skipping offset of them. Since one point
+ * uses two values, the number of "points" that are drawn is really (count >> 1).
+ * @param paint The paint used to draw the points
+ */
+ public void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count,
+ @NonNull Paint paint) {
+ super.drawPoints(pts, offset, count, paint);
+ }
+
+ /**
+ * Helper for drawPoints() that assumes you want to draw the entire array
+ */
+ public void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint) {
+ super.drawPoints(pts, paint);
+ }
+
+ /**
+ * Draw the text in the array, with each character's origin specified by the pos array.
+ *
+ * @param text The text to be drawn
+ * @param index The index of the first character to draw
+ * @param count The number of characters to draw, starting from index.
+ * @param pos Array of [x,y] positions, used to position each character
+ * @param paint The paint used for the text (e.g. color, size, style)
+ * @deprecated This method does not support glyph composition and decomposition and should
+ * therefore not be used to render complex scripts. It also doesn't handle
+ * supplementary characters (eg emoji).
+ */
+ @Deprecated
+ public void drawPosText(@NonNull char[] text, int index, int count,
+ @NonNull @Size(multiple = 2) float[] pos,
+ @NonNull Paint paint) {
+ super.drawPosText(text, index, count, pos, paint);
+ }
+
+ /**
+ * Draw the text in the array, with each character's origin specified by the pos array.
+ *
+ * @param text The text to be drawn
+ * @param pos Array of [x,y] positions, used to position each character
+ * @param paint The paint used for the text (e.g. color, size, style)
+ * @deprecated This method does not support glyph composition and decomposition and should
+ * therefore not be used to render complex scripts. It also doesn't handle
+ * supplementary characters (eg emoji).
+ */
+ @Deprecated
+ public void drawPosText(@NonNull String text, @NonNull @Size(multiple = 2) float[] pos,
+ @NonNull Paint paint) {
+ super.drawPosText(text, pos, paint);
+ }
+
+ /**
+ * Draw the specified Rect using the specified paint. The rectangle will be filled or framed
+ * based on the Style in the paint.
+ *
+ * @param rect The rect to be drawn
+ * @param paint The paint used to draw the rect
+ */
+ public void drawRect(@NonNull RectF rect, @NonNull Paint paint) {
+ super.drawRect(rect, paint);
+ }
+
+ /**
+ * Draw the specified Rect using the specified Paint. The rectangle will be filled or framed
+ * based on the Style in the paint.
+ *
+ * @param r The rectangle to be drawn.
+ * @param paint The paint used to draw the rectangle
+ */
+ public void drawRect(@NonNull Rect r, @NonNull Paint paint) {
+ super.drawRect(r, paint);
+ }
+
+ /**
+ * Draw the specified Rect using the specified paint. The rectangle will be filled or framed
+ * based on the Style in the paint.
+ *
+ * @param left The left side of the rectangle to be drawn
+ * @param top The top side of the rectangle to be drawn
+ * @param right The right side of the rectangle to be drawn
+ * @param bottom The bottom side of the rectangle to be drawn
+ * @param paint The paint used to draw the rect
+ */
+ public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) {
+ super.drawRect(left, top, right, bottom, paint);
+ }
+
+ /**
+ * Fill the entire canvas' bitmap (restricted to the current clip) with the specified RGB color,
+ * using srcover porterduff mode.
+ *
+ * @param r red component (0..255) of the color to draw onto the canvas
+ * @param g green component (0..255) of the color to draw onto the canvas
+ * @param b blue component (0..255) of the color to draw onto the canvas
+ */
+ public void drawRGB(int r, int g, int b) {
+ super.drawRGB(r, g, b);
+ }
+
+ /**
+ * Draw the specified round-rect using the specified paint. The roundrect will be filled or
+ * framed based on the Style in the paint.
+ *
+ * @param rect The rectangular bounds of the roundRect to be drawn
+ * @param rx The x-radius of the oval used to round the corners
+ * @param ry The y-radius of the oval used to round the corners
+ * @param paint The paint used to draw the roundRect
+ */
+ public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) {
+ super.drawRoundRect(rect, rx, ry, paint);
+ }
+
+ /**
+ * Draw the specified round-rect using the specified paint. The roundrect will be filled or
+ * framed based on the Style in the paint.
+ *
+ * @param rx The x-radius of the oval used to round the corners
+ * @param ry The y-radius of the oval used to round the corners
+ * @param paint The paint used to draw the roundRect
+ */
+ public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,
+ @NonNull Paint paint) {
+ super.drawRoundRect(left, top, right, bottom, rx, ry, paint);
+ }
+
+ /**
+ * Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted
+ * based on the Align setting in the paint.
+ *
+ * @param text The text to be drawn
+ * @param x The x-coordinate of the origin of the text being drawn
+ * @param y The y-coordinate of the baseline of the text being drawn
+ * @param paint The paint used for the text (e.g. color, size, style)
+ */
+ public void drawText(@NonNull char[] text, int index, int count, float x, float y,
+ @NonNull Paint paint) {
+ super.drawText(text, index, count, x, y, paint);
+ }
+
+ /**
+ * Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted
+ * based on the Align setting in the paint.
+ *
+ * @param text The text to be drawn
+ * @param x The x-coordinate of the origin of the text being drawn
+ * @param y The y-coordinate of the baseline of the text being drawn
+ * @param paint The paint used for the text (e.g. color, size, style)
+ */
+ public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
+ super.drawText(text, x, y, paint);
+ }
+
+ /**
+ * Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted
+ * based on the Align setting in the paint.
+ *
+ * @param text The text to be drawn
+ * @param start The index of the first character in text to draw
+ * @param end (end - 1) is the index of the last character in text to draw
+ * @param x The x-coordinate of the origin of the text being drawn
+ * @param y The y-coordinate of the baseline of the text being drawn
+ * @param paint The paint used for the text (e.g. color, size, style)
+ */
+ public void drawText(@NonNull String text, int start, int end, float x, float y,
+ @NonNull Paint paint) {
+ super.drawText(text, start, end, x, y, paint);
+ }
+
+ /**
+ * Draw the specified range of text, specified by start/end, with its origin at (x,y), in the
+ * specified Paint. The origin is interpreted based on the Align setting in the Paint.
+ *
+ * @param text The text to be drawn
+ * @param start The index of the first character in text to draw
+ * @param end (end - 1) is the index of the last character in text to draw
+ * @param x The x-coordinate of origin for where to draw the text
+ * @param y The y-coordinate of origin for where to draw the text
+ * @param paint The paint used for the text (e.g. color, size, style)
+ */
+ public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
+ @NonNull Paint paint) {
+ super.drawText(text, start, end, x, y, paint);
+ }
+
+ /**
+ * Draw the text, with origin at (x,y), using the specified paint, along the specified path. The
+ * paint's Align setting determins where along the path to start the text.
+ *
+ * @param text The text to be drawn
+ * @param path The path the text should follow for its baseline
+ * @param hOffset The distance along the path to add to the text's starting position
+ * @param vOffset The distance above(-) or below(+) the path to position the text
+ * @param paint The paint used for the text (e.g. color, size, style)
+ */
+ public void drawTextOnPath(@NonNull char[] text, int index, int count, @NonNull Path path,
+ float hOffset, float vOffset, @NonNull Paint paint) {
+ super.drawTextOnPath(text, index, count, path, hOffset, vOffset, paint);
+ }
+
+ /**
+ * Draw the text, with origin at (x,y), using the specified paint, along the specified path. The
+ * paint's Align setting determins where along the path to start the text.
+ *
+ * @param text The text to be drawn
+ * @param path The path the text should follow for its baseline
+ * @param hOffset The distance along the path to add to the text's starting position
+ * @param vOffset The distance above(-) or below(+) the path to position the text
+ * @param paint The paint used for the text (e.g. color, size, style)
+ */
+ public void drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset,
+ float vOffset, @NonNull Paint paint) {
+ super.drawTextOnPath(text, path, hOffset, vOffset, paint);
+ }
+
+ /**
+ * Draw a run of text, all in a single direction, with optional context for complex text
+ * shaping.
+ * <p>
+ * See {@link #drawTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint)} for
+ * more details. This method uses a character array rather than CharSequence to represent the
+ * string. Also, to be consistent with the pattern established in {@link #drawText}, in this
+ * method {@code count} and {@code contextCount} are used rather than offsets of the end
+ * position; {@code count = end - start, contextCount = contextEnd -
+ * contextStart}.
+ *
+ * @param text the text to render
+ * @param index the start of the text to render
+ * @param count the count of chars to render
+ * @param contextIndex the start of the context for shaping. Must be no greater than index.
+ * @param contextCount the number of characters in the context for shaping. contexIndex +
+ * contextCount must be no less than index + count.
+ * @param x the x position at which to draw the text
+ * @param y the y position at which to draw the text
+ * @param isRtl whether the run is in RTL direction
+ * @param paint the paint
+ */
+ public void drawTextRun(@NonNull char[] text, int index, int count, int contextIndex,
+ int contextCount, float x, float y, boolean isRtl, @NonNull Paint paint) {
+ super.drawTextRun(text, index, count, contextIndex, contextCount, x, y, isRtl, paint);
+ }
+
+ /**
+ * Draw a run of text, all in a single direction, with optional context for complex text
+ * shaping.
+ * <p>
+ * The run of text includes the characters from {@code start} to {@code end} in the text. In
+ * addition, the range {@code contextStart} to {@code contextEnd} is used as context for the
+ * purpose of complex text shaping, such as Arabic text potentially shaped differently based on
+ * the text next to it.
+ * <p>
+ * All text outside the range {@code contextStart..contextEnd} is ignored. The text between
+ * {@code start} and {@code end} will be laid out and drawn.
+ * <p>
+ * The direction of the run is explicitly specified by {@code isRtl}. Thus, this method is
+ * suitable only for runs of a single direction. Alignment of the text is as determined by the
+ * Paint's TextAlign value. Further, {@code 0 <= contextStart <= start <= end <= contextEnd
+ * <= text.length} must hold on entry.
+ * <p>
+ * Also see {@link android.graphics.Paint#getRunAdvance} for a corresponding method to measure
+ * the text; the advance width of the text drawn matches the value obtained from that method.
+ *
+ * @param text the text to render
+ * @param start the start of the text to render. Data before this position can be used for
+ * shaping context.
+ * @param end the end of the text to render. Data at or after this position can be used for
+ * shaping context.
+ * @param contextStart the index of the start of the shaping context
+ * @param contextEnd the index of the end of the shaping context
+ * @param x the x position at which to draw the text
+ * @param y the y position at which to draw the text
+ * @param isRtl whether the run is in RTL direction
+ * @param paint the paint
+ * @see #drawTextRun(char[], int, int, int, int, float, float, boolean, Paint)
+ */
+ public void drawTextRun(@NonNull CharSequence text, int start, int end, int contextStart,
+ int contextEnd, float x, float y, boolean isRtl, @NonNull Paint paint) {
+ super.drawTextRun(text, start, end, contextStart, contextEnd, x, y, isRtl, paint);
+ }
+
+ /**
+ * Draw the array of vertices, interpreted as triangles (based on mode). The verts array is
+ * required, and specifies the x,y pairs for each vertex. If texs is non-null, then it is used
+ * to specify the coordinate in shader coordinates to use at each vertex (the paint must have a
+ * shader in this case). If there is no texs array, but there is a color array, then each color
+ * is interpolated across its corresponding triangle in a gradient. If both texs and colors
+ * arrays are present, then they behave as before, but the resulting color at each pixels is the
+ * result of multiplying the colors from the shader and the color-gradient together. The indices
+ * array is optional, but if it is present, then it is used to specify the index of each
+ * triangle, rather than just walking through the arrays in order.
+ *
+ * @param mode How to interpret the array of vertices
+ * @param vertexCount The number of values in the vertices array (and corresponding texs and
+ * colors arrays if non-null). Each logical vertex is two values (x, y), vertexCount
+ * must be a multiple of 2.
+ * @param verts Array of vertices for the mesh
+ * @param vertOffset Number of values in the verts to skip before drawing.
+ * @param texs May be null. If not null, specifies the coordinates to sample into the current
+ * shader (e.g. bitmap tile or gradient)
+ * @param texOffset Number of values in texs to skip before drawing.
+ * @param colors May be null. If not null, specifies a color for each vertex, to be interpolated
+ * across the triangle.
+ * @param colorOffset Number of values in colors to skip before drawing.
+ * @param indices If not null, array of indices to reference into the vertex (texs, colors)
+ * array.
+ * @param indexCount number of entries in the indices array (if not null).
+ * @param paint Specifies the shader to use if the texs array is non-null.
+ */
+ public void drawVertices(@NonNull VertexMode mode, int vertexCount, @NonNull float[] verts,
+ int vertOffset, @Nullable float[] texs, int texOffset, @Nullable int[] colors,
+ int colorOffset, @Nullable short[] indices, int indexOffset, int indexCount,
+ @NonNull Paint paint) {
+ super.drawVertices(mode, vertexCount, verts, vertOffset, texs, texOffset,
+ colors, colorOffset, indices, indexOffset, indexCount, paint);
+ }
+}
diff --git a/android/graphics/CanvasProperty.java b/android/graphics/CanvasProperty.java
new file mode 100644
index 0000000..ea3886c
--- /dev/null
+++ b/android/graphics/CanvasProperty.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 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 android.graphics;
+
+import com.android.internal.util.VirtualRefBasePtr;
+
+/**
+ * TODO: Make public?
+ * @hide
+ */
+public final class CanvasProperty<T> {
+
+ private VirtualRefBasePtr mProperty;
+
+ public static CanvasProperty<Float> createFloat(float initialValue) {
+ return new CanvasProperty<Float>(nCreateFloat(initialValue));
+ }
+
+ public static CanvasProperty<Paint> createPaint(Paint initialValue) {
+ return new CanvasProperty<Paint>(nCreatePaint(initialValue.getNativeInstance()));
+ }
+
+ private CanvasProperty(long nativeContainer) {
+ mProperty = new VirtualRefBasePtr(nativeContainer);
+ }
+
+ /** @hide */
+ public long getNativeContainer() {
+ return mProperty.get();
+ }
+
+ private static native long nCreateFloat(float initialValue);
+ private static native long nCreatePaint(long initialValuePaintPtr);
+}
diff --git a/android/graphics/Canvas_Delegate.java b/android/graphics/Canvas_Delegate.java
new file mode 100644
index 0000000..9d8af38
--- /dev/null
+++ b/android/graphics/Canvas_Delegate.java
@@ -0,0 +1,481 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.layoutlib.bridge.impl.GcSnapshot;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.Nullable;
+import android.graphics.Bitmap.Config;
+
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.geom.AffineTransform;
+
+import libcore.util.NativeAllocationRegistry_Delegate;
+
+
+/**
+ * Delegate implementing the native methods of android.graphics.Canvas
+ *
+ * Through the layoutlib_create tool, the original native methods of Canvas have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original Canvas class.
+ *
+ * @see DelegateManager
+ *
+ */
+public final class Canvas_Delegate extends BaseCanvas_Delegate {
+
+ // ---- delegate manager ----
+ private static long sFinalizer = -1;
+
+ private DrawFilter_Delegate mDrawFilter = null;
+
+ // ---- Public Helper methods ----
+
+ /**
+ * Returns the native delegate associated to a given {@link Canvas} object.
+ */
+ public static Canvas_Delegate getDelegate(Canvas canvas) {
+ return (Canvas_Delegate) sManager.getDelegate(canvas.getNativeCanvasWrapper());
+ }
+
+ /**
+ * Returns the native delegate associated to a given an int referencing a {@link Canvas} object.
+ */
+ public static Canvas_Delegate getDelegate(long native_canvas) {
+ return (Canvas_Delegate) sManager.getDelegate(native_canvas);
+ }
+
+ /**
+ * Returns the current {@link Graphics2D} used to draw.
+ */
+ public GcSnapshot getSnapshot() {
+ return mSnapshot;
+ }
+
+ /**
+ * Returns the {@link DrawFilter} delegate or null if none have been set.
+ *
+ * @return the delegate or null.
+ */
+ public DrawFilter_Delegate getDrawFilter() {
+ return mDrawFilter;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static void nFreeCaches() {
+ // nothing to be done here.
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nFreeTextLayoutCaches() {
+ // nothing to be done here yet.
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nInitRaster(@Nullable Bitmap bitmap) {
+ long nativeBitmapOrZero = 0;
+ if (bitmap != null) {
+ nativeBitmapOrZero = bitmap.getNativeInstance();
+ }
+ if (nativeBitmapOrZero > 0) {
+ // get the Bitmap from the int
+ Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(nativeBitmapOrZero);
+
+ // create a new Canvas_Delegate with the given bitmap and return its new native int.
+ Canvas_Delegate newDelegate = new Canvas_Delegate(bitmapDelegate);
+
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // create a new Canvas_Delegate and return its new native int.
+ Canvas_Delegate newDelegate = new Canvas_Delegate();
+
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ public static void nSetBitmap(long canvas, Bitmap bitmap) {
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(canvas);
+ Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(bitmap);
+ if (canvasDelegate == null || bitmapDelegate==null) {
+ return;
+ }
+ canvasDelegate.mBitmap = bitmapDelegate;
+ canvasDelegate.mSnapshot = GcSnapshot.createDefaultSnapshot(bitmapDelegate);
+ }
+
+ @LayoutlibDelegate
+ public static boolean nIsOpaque(long nativeCanvas) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return false;
+ }
+
+ return canvasDelegate.mBitmap.getConfig() == Config.RGB_565;
+ }
+
+ @LayoutlibDelegate
+ public static int nGetWidth(long nativeCanvas) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return 0;
+ }
+
+ return canvasDelegate.mBitmap.getImage().getWidth();
+ }
+
+ @LayoutlibDelegate
+ public static int nGetHeight(long nativeCanvas) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return 0;
+ }
+
+ return canvasDelegate.mBitmap.getImage().getHeight();
+ }
+
+ @LayoutlibDelegate
+ public static int nSave(long nativeCanvas, int saveFlags) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return 0;
+ }
+
+ return canvasDelegate.save(saveFlags);
+ }
+
+ @LayoutlibDelegate
+ public static int nSaveLayer(long nativeCanvas, float l,
+ float t, float r, float b,
+ long paint, int layerFlags) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return 0;
+ }
+
+ Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(paint);
+ if (paintDelegate == null) {
+ return 0;
+ }
+
+ return canvasDelegate.saveLayer(new RectF(l, t, r, b),
+ paintDelegate, layerFlags);
+ }
+
+ @LayoutlibDelegate
+ public static int nSaveLayerAlpha(long nativeCanvas, float l,
+ float t, float r, float b,
+ int alpha, int layerFlags) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return 0;
+ }
+
+ return canvasDelegate.saveLayerAlpha(new RectF(l, t, r, b), alpha, layerFlags);
+ }
+
+ @LayoutlibDelegate
+ public static boolean nRestore(long nativeCanvas) {
+ // FIXME: implement throwOnUnderflow.
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return false;
+ }
+
+ canvasDelegate.restore();
+ return true;
+ }
+
+ @LayoutlibDelegate
+ public static void nRestoreToCount(long nativeCanvas, int saveCount) {
+ // FIXME: implement throwOnUnderflow.
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ canvasDelegate.restoreTo(saveCount);
+ }
+
+ @LayoutlibDelegate
+ public static int nGetSaveCount(long nativeCanvas) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return 0;
+ }
+
+ return canvasDelegate.getSnapshot().size();
+ }
+
+ @LayoutlibDelegate
+ public static void nTranslate(long nativeCanvas, float dx, float dy) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ canvasDelegate.getSnapshot().translate(dx, dy);
+ }
+
+ @LayoutlibDelegate
+ public static void nScale(long nativeCanvas, float sx, float sy) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ canvasDelegate.getSnapshot().scale(sx, sy);
+ }
+
+ @LayoutlibDelegate
+ public static void nRotate(long nativeCanvas, float degrees) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ canvasDelegate.getSnapshot().rotate(Math.toRadians(degrees));
+ }
+
+ @LayoutlibDelegate
+ public static void nSkew(long nativeCanvas, float kx, float ky) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ // get the current top graphics2D object.
+ GcSnapshot g = canvasDelegate.getSnapshot();
+
+ // get its current matrix
+ AffineTransform currentTx = g.getTransform();
+ // get the AffineTransform for the given skew.
+ float[] mtx = Matrix_Delegate.getSkew(kx, ky);
+ AffineTransform matrixTx = Matrix_Delegate.getAffineTransform(mtx);
+
+ // combine them so that the given matrix is applied after.
+ currentTx.preConcatenate(matrixTx);
+
+ // give it to the graphics2D as a new matrix replacing all previous transform
+ g.setTransform(currentTx);
+ }
+
+ @LayoutlibDelegate
+ public static void nConcat(long nCanvas, long nMatrix) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(nMatrix);
+ if (matrixDelegate == null) {
+ return;
+ }
+
+ // get the current top graphics2D object.
+ GcSnapshot snapshot = canvasDelegate.getSnapshot();
+
+ // get its current matrix
+ AffineTransform currentTx = snapshot.getTransform();
+ // get the AffineTransform of the given matrix
+ AffineTransform matrixTx = matrixDelegate.getAffineTransform();
+
+ // combine them so that the given matrix is applied after.
+ currentTx.concatenate(matrixTx);
+
+ // give it to the graphics2D as a new matrix replacing all previous transform
+ snapshot.setTransform(currentTx);
+ }
+
+ @LayoutlibDelegate
+ public static void nSetMatrix(long nCanvas, long nMatrix) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(nMatrix);
+ if (matrixDelegate == null) {
+ return;
+ }
+
+ // get the current top graphics2D object.
+ GcSnapshot snapshot = canvasDelegate.getSnapshot();
+
+ // get the AffineTransform of the given matrix
+ AffineTransform matrixTx = matrixDelegate.getAffineTransform();
+
+ // give it to the graphics2D as a new matrix replacing all previous transform
+ snapshot.setTransform(matrixTx);
+
+ if (matrixDelegate.hasPerspective()) {
+ assert false;
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_AFFINE,
+ "android.graphics.Canvas#setMatrix(android.graphics.Matrix) only " +
+ "supports affine transformations.", null, null /*data*/);
+ }
+ }
+
+ @LayoutlibDelegate
+ public static boolean nClipRect(long nCanvas,
+ float left, float top,
+ float right, float bottom,
+ int regionOp) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nCanvas);
+ if (canvasDelegate == null) {
+ return false;
+ }
+
+ return canvasDelegate.clipRect(left, top, right, bottom, regionOp);
+ }
+
+ @LayoutlibDelegate
+ public static boolean nClipPath(long nativeCanvas,
+ long nativePath,
+ int regionOp) {
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return true;
+ }
+
+ Path_Delegate pathDelegate = Path_Delegate.getDelegate(nativePath);
+ if (pathDelegate == null) {
+ return true;
+ }
+
+ return canvasDelegate.mSnapshot.clip(pathDelegate.getJavaShape(), regionOp);
+ }
+
+ @LayoutlibDelegate
+ public static void nSetDrawFilter(long nativeCanvas, long nativeFilter) {
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ canvasDelegate.mDrawFilter = DrawFilter_Delegate.getDelegate(nativeFilter);
+
+ if (canvasDelegate.mDrawFilter != null && !canvasDelegate.mDrawFilter.isSupported()) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_DRAWFILTER,
+ canvasDelegate.mDrawFilter.getSupportMessage(), null, null /*data*/);
+ }
+ }
+
+ @LayoutlibDelegate
+ public static boolean nGetClipBounds(long nativeCanvas,
+ Rect bounds) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
+ if (canvasDelegate == null) {
+ return false;
+ }
+
+ Rectangle rect = canvasDelegate.getSnapshot().getClip().getBounds();
+ if (rect != null && !rect.isEmpty()) {
+ bounds.left = rect.x;
+ bounds.top = rect.y;
+ bounds.right = rect.x + rect.width;
+ bounds.bottom = rect.y + rect.height;
+ return true;
+ }
+
+ return false;
+ }
+
+ @LayoutlibDelegate
+ public static void nGetMatrix(long canvas, long matrix) {
+ // get the delegate from the native int.
+ Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(canvas);
+ if (canvasDelegate == null) {
+ return;
+ }
+
+ Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix);
+ if (matrixDelegate == null) {
+ return;
+ }
+
+ AffineTransform transform = canvasDelegate.getSnapshot().getTransform();
+ matrixDelegate.set(Matrix_Delegate.makeValues(transform));
+ }
+
+ @LayoutlibDelegate
+ public static boolean nQuickReject(long nativeCanvas, long path) {
+ // FIXME properly implement quickReject
+ return false;
+ }
+
+ @LayoutlibDelegate
+ public static boolean nQuickReject(long nativeCanvas,
+ float left, float top,
+ float right, float bottom) {
+ // FIXME properly implement quickReject
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nGetNativeFinalizer() {
+ synchronized (Canvas_Delegate.class) {
+ if (sFinalizer == -1) {
+ sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(nativePtr -> {
+ Canvas_Delegate delegate = Canvas_Delegate.getDelegate(nativePtr);
+ if (delegate != null) {
+ delegate.dispose();
+ }
+ sManager.removeJavaReferenceFor(nativePtr);
+ });
+ }
+ }
+ return sFinalizer;
+ }
+
+ private Canvas_Delegate(Bitmap_Delegate bitmap) {
+ super(bitmap);
+ }
+
+ private Canvas_Delegate() {
+ super();
+ }
+}
+
diff --git a/android/graphics/Color.java b/android/graphics/Color.java
new file mode 100644
index 0000000..c4bf9d3
--- /dev/null
+++ b/android/graphics/Color.java
@@ -0,0 +1,1530 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.annotation.AnyThread;
+import android.annotation.ColorInt;
+import android.annotation.ColorLong;
+import android.annotation.HalfFloat;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.SuppressAutoDoc;
+import android.util.Half;
+import com.android.internal.util.XmlUtils;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.function.DoubleUnaryOperator;
+
+/**
+ * {@usesMathJax}
+ *
+ * <p>The <code>Color</code> class provides methods for creating, converting and
+ * manipulating colors. Colors have three different representations:</p>
+ * <ul>
+ * <li>Color ints, the most common representation</li>
+ * <li>Color longs</li>
+ * <li><code>Color</code> instances</li>
+ * </ul>
+ * <p>The section below describe each representation in detail.</p>
+ *
+ * <h3>Color ints</h3>
+ * <p>Color ints are the most common representation of colors on Android and
+ * have been used since {@link android.os.Build.VERSION_CODES#BASE API level 1}.</p>
+ *
+ * <p>A color int always defines a color in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space using 4 components packed in a single 32 bit integer value:</p>
+ *
+ * <table summary="Color int definition">
+ * <tr>
+ * <th>Component</th><th>Name</th><th>Size</th><th>Range</th>
+ * </tr>
+ * <tr><td>A</td><td>Alpha</td><td>8 bits</td><td>\([0..255]\)</td></tr>
+ * <tr><td>R</td><td>Red</td><td>8 bits</td><td>\([0..255]\)</td></tr>
+ * <tr><td>G</td><td>Green</td><td>8 bits</td><td>\([0..255]\)</td></tr>
+ * <tr><td>B</td><td>Blue</td><td>8 bits</td><td>\([0..255]\)</td></tr>
+ * </table>
+ *
+ * <p>The components in this table are listed in encoding order (see below),
+ * which is why color ints are called ARGB colors.</p>
+ *
+ * <h4>Usage in code</h4>
+ * <p>To avoid confusing color ints with arbitrary integer values, it is a
+ * good practice to annotate them with the <code>@ColorInt</code> annotation
+ * found in the Android Support Library.</p>
+ *
+ * <h4>Encoding</h4>
+ * <p>The four components of a color int are encoded in the following way:</p>
+ * <pre class="prettyprint">
+ * int color = (A & 0xff) << 24 | (R & 0xff) << 16 | (G & 0xff) << 8 | (B & 0xff);
+ * </pre>
+ *
+ * <p>Because of this encoding, color ints can easily be described as an integer
+ * constant in source. For instance, opaque blue is <code>0xff0000ff</code>
+ * and yellow is <code>0xffffff00</code>.</p>
+ *
+ * <p>To easily encode color ints, it is recommended to use the static methods
+ * {@link #argb(int, int, int, int)} and {@link #rgb(int, int, int)}. The second
+ * method omits the alpha component and assumes the color is opaque (alpha is 255).
+ * As a convenience this class also offers methods to encode color ints from components
+ * defined in the \([0..1]\) range: {@link #argb(float, float, float, float)} and
+ * {@link #rgb(float, float, float)}.</p>
+ *
+ * <p>Color longs (defined below) can be easily converted to color ints by invoking
+ * the {@link #toArgb(long)} method. This method performs a color space conversion
+ * if needed.</p>
+ *
+ * <p>It is also possible to create a color int by invoking the method {@link #toArgb()}
+ * on a color instance.</p>
+ *
+ * <h4>Decoding</h4>
+ * <p>The four ARGB components can be individually extracted from a color int
+ * using the following expressions:</p>
+ * <pre class="prettyprint">
+ * int A = (color >> 24) & 0xff; // or color >>> 24
+ * int R = (color >> 16) & 0xff;
+ * int G = (color >> 8) & 0xff;
+ * int B = (color ) & 0xff;
+ * </pre>
+ *
+ * <p>This class offers convenience methods to easily extract these components:</p>
+ * <ul>
+ * <li>{@link #alpha(int)} to extract the alpha component</li>
+ * <li>{@link #red(int)} to extract the red component</li>
+ * <li>{@link #green(int)} to extract the green component</li>
+ * <li>{@link #blue(int)} to extract the blue component</li>
+ * </ul>
+ *
+ * <h3>Color longs</h3>
+ * <p>Color longs are a representation introduced in
+ * {@link android.os.Build.VERSION_CODES#O Android O} to store colors in different
+ * {@link ColorSpace color spaces}, with more precision than color ints.</p>
+ *
+ * <p>A color long always defines a color using 4 components packed in a single
+ * 64 bit long value. One of these components is always alpha while the other
+ * three components depend on the color space's {@link ColorSpace.Model color model}.
+ * The most common color model is the {@link ColorSpace.Model#RGB RGB} model in
+ * which the components represent red, green and blue values.</p>
+ *
+ * <p class="note"><b>Component ranges:</b> the ranges defined in the tables
+ * below indicate the ranges that can be encoded in a color long. They do not
+ * represent the actual ranges as they may differ per color space. For instance,
+ * the RGB components of a color in the {@link ColorSpace.Named#DISPLAY_P3 Display P3}
+ * color space use the \([0..1]\) range. Please refer to the documentation of the
+ * various {@link ColorSpace.Named color spaces} to find their respective ranges.</p>
+ *
+ * <p class="note"><b>Alpha range:</b> while alpha is encoded in a color long using
+ * a 10 bit integer (thus using a range of \([0..1023]\)), it is converted to and
+ * from \([0..1]\) float values when decoding and encoding color longs.</p>
+ *
+ * <p class="note"><b>sRGB color space:</b> for compatibility reasons and ease of
+ * use, color longs encoding {@link ColorSpace.Named#SRGB sRGB} colors do not
+ * use the same encoding as other color longs.</p>
+ *
+ * <table summary="Color long definition">
+ * <tr>
+ * <th>Component</th><th>Name</th><th>Size</th><th>Range</th>
+ * </tr>
+ * <tr><td colspan="4">{@link ColorSpace.Model#RGB RGB} color model</td></tr>
+ * <tr><td>R</td><td>Red</td><td>16 bits</td><td>\([-65504.0, 65504.0]\)</td></tr>
+ * <tr><td>G</td><td>Green</td><td>16 bits</td><td>\([-65504.0, 65504.0]\)</td></tr>
+ * <tr><td>B</td><td>Blue</td><td>16 bits</td><td>\([-65504.0, 65504.0]\)</td></tr>
+ * <tr><td>A</td><td>Alpha</td><td>10 bits</td><td>\([0..1023]\)</td></tr>
+ * <tr><td></td><td>Color space</td><td>6 bits</td><td>\([0..63]\)</td></tr>
+ * <tr><td colspan="4">{@link ColorSpace.Named#SRGB sRGB} color space</td></tr>
+ * <tr><td>A</td><td>Alpha</td><td>8 bits</td><td>\([0..255]\)</td></tr>
+ * <tr><td>R</td><td>Red</td><td>8 bits</td><td>\([0..255]\)</td></tr>
+ * <tr><td>G</td><td>Green</td><td>8 bits</td><td>\([0..255]\)</td></tr>
+ * <tr><td>B</td><td>Blue</td><td>8 bits</td><td>\([0..255]\)</td></tr>
+ * <tr><td>X</td><td>Unused</td><td>32 bits</td><td>\(0\)</td></tr>
+ * <tr><td colspan="4">{@link ColorSpace.Model#XYZ XYZ} color model</td></tr>
+ * <tr><td>X</td><td>X</td><td>16 bits</td><td>\([-65504.0, 65504.0]\)</td></tr>
+ * <tr><td>Y</td><td>Y</td><td>16 bits</td><td>\([-65504.0, 65504.0]\)</td></tr>
+ * <tr><td>Z</td><td>Z</td><td>16 bits</td><td>\([-65504.0, 65504.0]\)</td></tr>
+ * <tr><td>A</td><td>Alpha</td><td>10 bits</td><td>\([0..1023]\)</td></tr>
+ * <tr><td></td><td>Color space</td><td>6 bits</td><td>\([0..63]\)</td></tr>
+ * <tr><td colspan="4">{@link ColorSpace.Model#XYZ Lab} color model</td></tr>
+ * <tr><td>L</td><td>L</td><td>16 bits</td><td>\([-65504.0, 65504.0]\)</td></tr>
+ * <tr><td>a</td><td>a</td><td>16 bits</td><td>\([-65504.0, 65504.0]\)</td></tr>
+ * <tr><td>b</td><td>b</td><td>16 bits</td><td>\([-65504.0, 65504.0]\)</td></tr>
+ * <tr><td>A</td><td>Alpha</td><td>10 bits</td><td>\([0..1023]\)</td></tr>
+ * <tr><td></td><td>Color space</td><td>6 bits</td><td>\([0..63]\)</td></tr>
+ * <tr><td colspan="4">{@link ColorSpace.Model#CMYK CMYK} color model</td></tr>
+ * <tr><td colspan="4">Unsupported</td></tr>
+ * </table>
+ *
+ * <p>The components in this table are listed in encoding order (see below),
+ * which is why color longs in the RGB model are called RGBA colors (even if
+ * this doesn't quite hold for the special case of sRGB colors).</p>
+ *
+ * <p>The color long encoding relies on half-precision float values (fp16). If you
+ * wish to know more about the limitations of half-precision float values, please
+ * refer to the documentation of the {@link Half} class.</p>
+ *
+ * <h4>Usage in code</h4>
+ * <p>To avoid confusing color longs with arbitrary long values, it is a
+ * good practice to annotate them with the <code>@ColorLong</code> annotation
+ * found in the Android Support Library.</p>
+ *
+ * <h4>Encoding</h4>
+ *
+ * <p>Given the complex nature of color longs, it is strongly encouraged to use
+ * the various methods provided by this class to encode them.</p>
+ *
+ * <p>The most flexible way to encode a color long is to use the method
+ * {@link #pack(float, float, float, float, ColorSpace)}. This method allows you
+ * to specify three color components (typically RGB), an alpha component and a
+ * color space. To encode sRGB colors, use {@link #pack(float, float, float)}
+ * and {@link #pack(float, float, float, float)} which are the
+ * equivalent of {@link #rgb(int, int, int)} and {@link #argb(int, int, int, int)}
+ * for color ints. If you simply need to convert a color int into a color long,
+ * use {@link #pack(int)}.</p>
+ *
+ * <p>It is also possible to create a color long value by invoking the method
+ * {@link #pack()} on a color instance.</p>
+ *
+ * <h4>Decoding</h4>
+ *
+ * <p>This class offers convenience methods to easily extract the components
+ * of a color long:</p>
+ * <ul>
+ * <li>{@link #alpha(long)} to extract the alpha component</li>
+ * <li>{@link #red(long)} to extract the red/X/L component</li>
+ * <li>{@link #green(long)} to extract the green/Y/a component</li>
+ * <li>{@link #blue(long)} to extract the blue/Z/b component</li>
+ * </ul>
+ *
+ * <p>The values returned by these methods depend on the color space encoded
+ * in the color long. The values are however typically in the \([0..1]\) range
+ * for RGB colors. Please refer to the documentation of the various
+ * {@link ColorSpace.Named color spaces} for the exact ranges.</p>
+ *
+ * <h3>Color instances</h3>
+ * <p>Color instances are a representation introduced in
+ * {@link android.os.Build.VERSION_CODES#O Android O} to store colors in different
+ * {@link ColorSpace color spaces}, with more precision than both color ints and
+ * color longs. Color instances also offer the ability to store more than 4
+ * components if necessary.</p>
+ *
+ * <p>Colors instances are immutable and can be created using one of the various
+ * <code>valueOf</code> methods. For instance:</p>
+ * <pre class="prettyprint">
+ * // sRGB
+ * Color opaqueRed = Color.valueOf(0xffff0000); // from a color int
+ * Color translucentRed = Color.valueOf(1.0f, 0.0f, 0.0f, 0.5f);
+ *
+ * // Wide gamut color
+ * {@literal @}ColorLong long p3 = pack(1.0f, 1.0f, 0.0f, 1.0f, colorSpaceP3);
+ * Color opaqueYellow = Color.valueOf(p3); // from a color long
+ *
+ * // CIE L*a*b* color space
+ * ColorSpace lab = ColorSpace.get(ColorSpace.Named.LAB);
+ * Color green = Color.valueOf(100.0f, -128.0f, 128.0f, 1.0f, lab);
+ * </pre>
+ *
+ * <p>Color instances can be converted to color ints ({@link #toArgb()}) or
+ * color longs ({@link #pack()}). They also offer easy access to their various
+ * components using the following methods:</p>
+ * <ul>
+ * <li>{@link #alpha()}, returns the alpha component value</li>
+ * <li>{@link #red()}, returns the red component value (or first
+ * component value in non-RGB models)</li>
+ * <li>{@link #green()}, returns the green component value (or second
+ * component value in non-RGB models)</li>
+ * <li>{@link #blue()}, returns the blue component value (or third
+ * component value in non-RGB models)</li>
+ * <li>{@link #getComponent(int)}, returns a specific component value</li>
+ * <li>{@link #getComponents()}, returns all component values as an array</li>
+ * </ul>
+ *
+ * <h3>Color space conversions</h3>
+ * <p>You can convert colors from one color space to another using
+ * {@link ColorSpace#connect(ColorSpace, ColorSpace)} and its variants. However,
+ * the <code>Color</code> class provides a few convenience methods to simplify
+ * the process. Here is a brief description of some of them:</p>
+ * <ul>
+ * <li>{@link #convert(ColorSpace)} to convert a color instance in a color
+ * space to a new color instance in a different color space</li>
+ * <li>{@link #convert(float, float, float, float, ColorSpace, ColorSpace)} to
+ * convert a color from a source color space to a destination color space</li>
+ * <li>{@link #convert(long, ColorSpace)} to convert a color long from its
+ * built-in color space to a destination color space</li>
+ * <li>{@link #convert(int, ColorSpace)} to convert a color int from sRGB
+ * to a destination color space</li>
+ * </ul>
+ *
+ * <p>Please refere to the {@link ColorSpace} documentation for more
+ * information.</p>
+ *
+ * <h3>Alpha and transparency</h3>
+ * <p>The alpha component of a color defines the level of transparency of a
+ * color. When the alpha component is 0, the color is completely transparent.
+ * When the alpha is component is 1 (in the \([0..1]\) range) or 255 (in the
+ * \([0..255]\) range), the color is completely opaque.</p>
+ *
+ * <p>The color representations described above do not use pre-multiplied
+ * color components (a pre-multiplied color component is a color component
+ * that has been multiplied by the value of the alpha component).
+ * For instance, the color int representation of opaque red is
+ * <code>0xffff0000</code>. For semi-transparent (50%) red, the
+ * representation becomes <code>0x80ff0000</code>. The equivalent color
+ * instance representations would be <code>(1.0, 0.0, 0.0, 1.0)</code>
+ * and <code>(1.0, 0.0, 0.0, 0.5)</code>.</p>
+ */
+@AnyThread
+@SuppressAutoDoc
+public class Color {
+ @ColorInt public static final int BLACK = 0xFF000000;
+ @ColorInt public static final int DKGRAY = 0xFF444444;
+ @ColorInt public static final int GRAY = 0xFF888888;
+ @ColorInt public static final int LTGRAY = 0xFFCCCCCC;
+ @ColorInt public static final int WHITE = 0xFFFFFFFF;
+ @ColorInt public static final int RED = 0xFFFF0000;
+ @ColorInt public static final int GREEN = 0xFF00FF00;
+ @ColorInt public static final int BLUE = 0xFF0000FF;
+ @ColorInt public static final int YELLOW = 0xFFFFFF00;
+ @ColorInt public static final int CYAN = 0xFF00FFFF;
+ @ColorInt public static final int MAGENTA = 0xFFFF00FF;
+ @ColorInt public static final int TRANSPARENT = 0;
+
+ @NonNull
+ @Size(min = 4, max = 5)
+ private final float[] mComponents;
+
+ @NonNull
+ private final ColorSpace mColorSpace;
+
+ /**
+ * Creates a new color instance set to opaque black in the
+ * {@link ColorSpace.Named#SRGB sRGB} color space.
+ *
+ * @see #valueOf(float, float, float)
+ * @see #valueOf(float, float, float, float)
+ * @see #valueOf(float, float, float, float, ColorSpace)
+ * @see #valueOf(float[], ColorSpace)
+ * @see #valueOf(int)
+ * @see #valueOf(long)
+ */
+ public Color() {
+ // This constructor is required for compatibility with previous APIs
+ mComponents = new float[] { 0.0f, 0.0f, 0.0f, 1.0f };
+ mColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+ }
+
+ /**
+ * Creates a new color instance in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @param r The value of the red channel, must be in [0..1] range
+ * @param g The value of the green channel, must be in [0..1] range
+ * @param b The value of the blue channel, must be in [0..1] range
+ * @param a The value of the alpha channel, must be in [0..1] range
+ */
+ private Color(float r, float g, float b, float a) {
+ this(r, g, b, a, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+
+ /**
+ * Creates a new color instance in the specified color space. The color space
+ * must have a 3 components model.
+ *
+ * @param r The value of the red channel, must be in the color space defined range
+ * @param g The value of the green channel, must be in the color space defined range
+ * @param b The value of the blue channel, must be in the color space defined range
+ * @param a The value of the alpha channel, must be in [0..1] range
+ * @param colorSpace This color's color space, cannot be null
+ */
+ private Color(float r, float g, float b, float a, @NonNull ColorSpace colorSpace) {
+ mComponents = new float[] { r, g, b, a };
+ mColorSpace = colorSpace;
+ }
+
+ /**
+ * Creates a new color instance in the specified color space.
+ *
+ * @param components An array of color components, plus alpha
+ * @param colorSpace This color's color space, cannot be null
+ */
+ private Color(@Size(min = 4, max = 5) float[] components, @NonNull ColorSpace colorSpace) {
+ mComponents = components;
+ mColorSpace = colorSpace;
+ }
+
+ /**
+ * Returns this color's color space.
+ *
+ * @return A non-null instance of {@link ColorSpace}
+ */
+ @NonNull
+ public ColorSpace getColorSpace() {
+ return mColorSpace;
+ }
+
+ /**
+ * Returns the color model of this color.
+ *
+ * @return A non-null {@link ColorSpace.Model}
+ */
+ public ColorSpace.Model getModel() {
+ return mColorSpace.getModel();
+ }
+
+ /**
+ * Indicates whether this color color is in a wide-gamut color space.
+ * See {@link ColorSpace#isWideGamut()} for a definition of a wide-gamut
+ * color space.
+ *
+ * @return True if this color is in a wide-gamut color space, false otherwise
+ *
+ * @see #isSrgb()
+ * @see ColorSpace#isWideGamut()
+ */
+ public boolean isWideGamut() {
+ return getColorSpace().isWideGamut();
+ }
+
+ /**
+ * Indicates whether this color is in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @return True if this color is in the sRGB color space, false otherwise
+ *
+ * @see #isWideGamut()
+ */
+ public boolean isSrgb() {
+ return getColorSpace().isSrgb();
+ }
+
+ /**
+ * Returns the number of components that form a color value according
+ * to this color space's color model, plus one extra component for
+ * alpha.
+ *
+ * @return The integer 4 or 5
+ */
+ @IntRange(from = 4, to = 5)
+ public int getComponentCount() {
+ return mColorSpace.getComponentCount() + 1;
+ }
+
+ /**
+ * Packs this color into a color long. See the documentation of this class
+ * for a description of the color long format.
+ *
+ * @return A color long
+ *
+ * @throws IllegalArgumentException If this color's color space has the id
+ * {@link ColorSpace#MIN_ID} or if this color has more than 4 components
+ */
+ @ColorLong
+ public long pack() {
+ return pack(mComponents[0], mComponents[1], mComponents[2], mComponents[3], mColorSpace);
+ }
+
+ /**
+ * Converts this color from its color space to the specified color space.
+ * The conversion is done using the default rendering intent as specified
+ * by {@link ColorSpace#connect(ColorSpace, ColorSpace)}.
+ *
+ * @param colorSpace The destination color space, cannot be null
+ *
+ * @return A non-null color instance in the specified color space
+ */
+ @NonNull
+ public Color convert(@NonNull ColorSpace colorSpace) {
+ ColorSpace.Connector connector = ColorSpace.connect(mColorSpace, colorSpace);
+ float[] color = new float[] {
+ mComponents[0], mComponents[1], mComponents[2], mComponents[3]
+ };
+ connector.transform(color);
+ return new Color(color, colorSpace);
+ }
+
+ /**
+ * Converts this color to an ARGB color int. A color int is always in
+ * the {@link ColorSpace.Named#SRGB sRGB} color space. This implies
+ * a color space conversion is applied if needed.
+ *
+ * @return An ARGB color in the sRGB color space
+ */
+ @ColorInt
+ public int toArgb() {
+ if (mColorSpace.isSrgb()) {
+ return ((int) (mComponents[3] * 255.0f + 0.5f) << 24) |
+ ((int) (mComponents[0] * 255.0f + 0.5f) << 16) |
+ ((int) (mComponents[1] * 255.0f + 0.5f) << 8) |
+ (int) (mComponents[2] * 255.0f + 0.5f);
+ }
+
+ float[] color = new float[] {
+ mComponents[0], mComponents[1], mComponents[2], mComponents[3]
+ };
+ // The transformation saturates the output
+ ColorSpace.connect(mColorSpace).transform(color);
+
+ return ((int) (color[3] * 255.0f + 0.5f) << 24) |
+ ((int) (color[0] * 255.0f + 0.5f) << 16) |
+ ((int) (color[1] * 255.0f + 0.5f) << 8) |
+ (int) (color[2] * 255.0f + 0.5f);
+ }
+
+ /**
+ * <p>Returns the value of the red component in the range defined by this
+ * color's color space (see {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}).</p>
+ *
+ * <p>If this color's color model is not {@link ColorSpace.Model#RGB RGB},
+ * calling this method is equivalent to <code>getComponent(0)</code>.</p>
+ *
+ * @see #alpha()
+ * @see #red()
+ * @see #green
+ * @see #getComponents()
+ */
+ public float red() {
+ return mComponents[0];
+ }
+
+ /**
+ * <p>Returns the value of the green component in the range defined by this
+ * color's color space (see {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}).</p>
+ *
+ * <p>If this color's color model is not {@link ColorSpace.Model#RGB RGB},
+ * calling this method is equivalent to <code>getComponent(1)</code>.</p>
+ *
+ * @see #alpha()
+ * @see #red()
+ * @see #green
+ * @see #getComponents()
+ */
+ public float green() {
+ return mComponents[1];
+ }
+
+ /**
+ * <p>Returns the value of the blue component in the range defined by this
+ * color's color space (see {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}).</p>
+ *
+ * <p>If this color's color model is not {@link ColorSpace.Model#RGB RGB},
+ * calling this method is equivalent to <code>getComponent(2)</code>.</p>
+ *
+ * @see #alpha()
+ * @see #red()
+ * @see #green
+ * @see #getComponents()
+ */
+ public float blue() {
+ return mComponents[2];
+ }
+
+ /**
+ * Returns the value of the alpha component in the range \([0..1]\).
+ * Calling this method is equivalent to
+ * <code>getComponent(getComponentCount() - 1)</code>.
+ *
+ * @see #red()
+ * @see #green()
+ * @see #blue()
+ * @see #getComponents()
+ * @see #getComponent(int)
+ */
+ public float alpha() {
+ return mComponents[mComponents.length - 1];
+ }
+
+ /**
+ * Returns this color's components as a new array. The last element of the
+ * array is always the alpha component.
+ *
+ * @return A new, non-null array whose size is equal to {@link #getComponentCount()}
+ *
+ * @see #getComponent(int)
+ */
+ @NonNull
+ @Size(min = 4, max = 5)
+ public float[] getComponents() {
+ return Arrays.copyOf(mComponents, mComponents.length);
+ }
+
+ /**
+ * Copies this color's components in the supplied array. The last element of the
+ * array is always the alpha component.
+ *
+ * @param components An array of floats whose size must be at least
+ * {@link #getComponentCount()}, can be null
+ * @return The array passed as a parameter if not null, or a new array of length
+ * {@link #getComponentCount()}
+ *
+ * @see #getComponent(int)
+ *
+ * @throws IllegalArgumentException If the specified array's length is less than
+ * {@link #getComponentCount()}
+ */
+ @NonNull
+ @Size(min = 4)
+ public float[] getComponents(@Nullable @Size(min = 4) float[] components) {
+ if (components == null) {
+ return Arrays.copyOf(mComponents, mComponents.length);
+ }
+
+ if (components.length < mComponents.length) {
+ throw new IllegalArgumentException("The specified array's length must be at "
+ + "least " + mComponents.length);
+ }
+
+ System.arraycopy(mComponents, 0, components, 0, mComponents.length);
+ return components;
+ }
+
+ /**
+ * <p>Returns the value of the specified component in the range defined by
+ * this color's color space (see {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}).</p>
+ *
+ * <p>If the requested component index is {@link #getComponentCount()},
+ * this method returns the alpha component, always in the range
+ * \([0..1]\).</p>
+ *
+ * @see #getComponents()
+ *
+ * @throws ArrayIndexOutOfBoundsException If the specified component index
+ * is < 0 or >= {@link #getComponentCount()}
+ */
+ public float getComponent(@IntRange(from = 0, to = 4) int component) {
+ return mComponents[component];
+ }
+
+ /**
+ * <p>Returns the relative luminance of this color.</p>
+ *
+ * <p>Based on the formula for relative luminance defined in WCAG 2.0,
+ * W3C Recommendation 11 December 2008.</p>
+ *
+ * @return A value between 0 (darkest black) and 1 (lightest white)
+ *
+ * @throws IllegalArgumentException If the this color's color space
+ * does not use the {@link ColorSpace.Model#RGB RGB} color model
+ */
+ public float luminance() {
+ if (mColorSpace.getModel() != ColorSpace.Model.RGB) {
+ throw new IllegalArgumentException("The specified color must be encoded in an RGB " +
+ "color space. The supplied color space is " + mColorSpace.getModel());
+ }
+
+ DoubleUnaryOperator eotf = ((ColorSpace.Rgb) mColorSpace).getEotf();
+ double r = eotf.applyAsDouble(mComponents[0]);
+ double g = eotf.applyAsDouble(mComponents[1]);
+ double b = eotf.applyAsDouble(mComponents[2]);
+
+ return saturate((float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Color color = (Color) o;
+
+ //noinspection SimplifiableIfStatement
+ if (!Arrays.equals(mComponents, color.mComponents)) return false;
+ return mColorSpace.equals(color.mColorSpace);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Arrays.hashCode(mComponents);
+ result = 31 * result + mColorSpace.hashCode();
+ return result;
+ }
+
+ /**
+ * <p>Returns a string representation of the object. This method returns
+ * a string equal to the value of:</p>
+ *
+ * <pre class="prettyprint">
+ * "Color(" + r + ", " + g + ", " + b + ", " + a +
+ * ", " + getColorSpace().getName + ')'
+ * </pre>
+ *
+ * <p>For instance, the string representation of opaque black in the sRGB
+ * color space is equal to the following value:</p>
+ *
+ * <pre>
+ * Color(0.0, 0.0, 0.0, 1.0, sRGB IEC61966-2.1)
+ * </pre>
+ *
+ * @return A non-null string representation of the object
+ */
+ @Override
+ @NonNull
+ public String toString() {
+ StringBuilder b = new StringBuilder("Color(");
+ for (float c : mComponents) {
+ b.append(c).append(", ");
+ }
+ b.append(mColorSpace.getName());
+ b.append(')');
+ return b.toString();
+ }
+
+ /**
+ * Returns the color space encoded in the specified color long.
+ *
+ * @param color The color long whose color space to extract
+ * @return A non-null color space instance
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ *
+ * @see #red(long)
+ * @see #green(long)
+ * @see #blue(long)
+ * @see #alpha(long)
+ */
+ @NonNull
+ public static ColorSpace colorSpace(@ColorLong long color) {
+ return ColorSpace.get((int) (color & 0x3fL));
+ }
+
+ /**
+ * Returns the red component encoded in the specified color long.
+ * The range of the returned value depends on the color space
+ * associated with the specified color. The color space can be
+ * queried by calling {@link #colorSpace(long)}.
+ *
+ * @param color The color long whose red channel to extract
+ * @return A float value with a range defined by the specified color's
+ * color space
+ *
+ * @see #colorSpace(long)
+ * @see #green(long)
+ * @see #blue(long)
+ * @see #alpha(long)
+ */
+ public static float red(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 48) & 0xff) / 255.0f;
+ return Half.toFloat((short) ((color >> 48) & 0xffff));
+ }
+
+ /**
+ * Returns the green component encoded in the specified color long.
+ * The range of the returned value depends on the color space
+ * associated with the specified color. The color space can be
+ * queried by calling {@link #colorSpace(long)}.
+ *
+ * @param color The color long whose green channel to extract
+ * @return A float value with a range defined by the specified color's
+ * color space
+ *
+ * @see #colorSpace(long)
+ * @see #red(long)
+ * @see #blue(long)
+ * @see #alpha(long)
+ */
+ public static float green(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 40) & 0xff) / 255.0f;
+ return Half.toFloat((short) ((color >> 32) & 0xffff));
+ }
+
+ /**
+ * Returns the blue component encoded in the specified color long.
+ * The range of the returned value depends on the color space
+ * associated with the specified color. The color space can be
+ * queried by calling {@link #colorSpace(long)}.
+ *
+ * @param color The color long whose blue channel to extract
+ * @return A float value with a range defined by the specified color's
+ * color space
+ *
+ * @see #colorSpace(long)
+ * @see #red(long)
+ * @see #green(long)
+ * @see #alpha(long)
+ */
+ public static float blue(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 32) & 0xff) / 255.0f;
+ return Half.toFloat((short) ((color >> 16) & 0xffff));
+ }
+
+ /**
+ * Returns the alpha component encoded in the specified color long.
+ * The returned value is always in the range \([0..1]\).
+ *
+ * @param color The color long whose blue channel to extract
+ * @return A float value in the range \([0..1]\)
+ *
+ * @see #colorSpace(long)
+ * @see #red(long)
+ * @see #green(long)
+ * @see #blue(long)
+ */
+ public static float alpha(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 56) & 0xff) / 255.0f;
+ return ((color >> 6) & 0x3ff) / 1023.0f;
+ }
+
+ /**
+ * Indicates whether the specified color is in the
+ * {@link ColorSpace.Named#SRGB sRGB} color space.
+ *
+ * @param color The color to test
+ * @return True if the color is in the sRGB color space, false otherwise
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ *
+ * @see #isInColorSpace(long, ColorSpace)
+ * @see #isWideGamut(long)
+ */
+ public static boolean isSrgb(@ColorLong long color) {
+ return colorSpace(color).isSrgb();
+ }
+
+ /**
+ * Indicates whether the specified color is in a wide-gamut color space.
+ * See {@link ColorSpace#isWideGamut()} for a definition of a wide-gamut
+ * color space.
+ *
+ * @param color The color to test
+ * @return True if the color is in a wide-gamut color space, false otherwise
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ *
+ * @see #isInColorSpace(long, ColorSpace)
+ * @see #isSrgb(long)
+ * @see ColorSpace#isWideGamut()
+ */
+ public static boolean isWideGamut(@ColorLong long color) {
+ return colorSpace(color).isWideGamut();
+ }
+
+ /**
+ * Indicates whether the specified color is in the specified color space.
+ *
+ * @param color The color to test
+ * @param colorSpace The color space to test against
+ * @return True if the color is in the specified color space, false otherwise
+ *
+ * @see #isSrgb(long)
+ * @see #isWideGamut(long)
+ */
+ public static boolean isInColorSpace(@ColorLong long color, @NonNull ColorSpace colorSpace) {
+ return (int) (color & 0x3fL) == colorSpace.getId();
+ }
+
+ /**
+ * Converts the specified color long to an ARGB color int. A color int is
+ * always in the {@link ColorSpace.Named#SRGB sRGB} color space. This implies
+ * a color space conversion is applied if needed.
+ *
+ * @return An ARGB color in the sRGB color space
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ */
+ @ColorInt
+ public static int toArgb(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return (int) (color >> 32);
+
+ float r = red(color);
+ float g = green(color);
+ float b = blue(color);
+ float a = alpha(color);
+
+ // The transformation saturates the output
+ float[] c = ColorSpace.connect(colorSpace(color)).transform(r, g, b);
+
+ return ((int) (a * 255.0f + 0.5f) << 24) |
+ ((int) (c[0] * 255.0f + 0.5f) << 16) |
+ ((int) (c[1] * 255.0f + 0.5f) << 8) |
+ (int) (c[2] * 255.0f + 0.5f);
+ }
+
+ /**
+ * Creates a new <code>Color</code> instance from an ARGB color int.
+ * The resulting color is in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @param color The ARGB color int to create a <code>Color</code> from
+ * @return A non-null instance of {@link Color}
+ */
+ @NonNull
+ public static Color valueOf(@ColorInt int color) {
+ float r = ((color >> 16) & 0xff) / 255.0f;
+ float g = ((color >> 8) & 0xff) / 255.0f;
+ float b = ((color ) & 0xff) / 255.0f;
+ float a = ((color >> 24) & 0xff) / 255.0f;
+ return new Color(r, g, b, a, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+
+ /**
+ * Creates a new <code>Color</code> instance from a color long.
+ * The resulting color is in the same color space as the specified color long.
+ *
+ * @param color The color long to create a <code>Color</code> from
+ * @return A non-null instance of {@link Color}
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ */
+ @NonNull
+ public static Color valueOf(@ColorLong long color) {
+ return new Color(red(color), green(color), blue(color), alpha(color), colorSpace(color));
+ }
+
+ /**
+ * Creates a new opaque <code>Color</code> in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space with the specified red, green and blue component values. The component
+ * values must be in the range \([0..1]\).
+ *
+ * @param r The red component of the opaque sRGB color to create, in \([0..1]\)
+ * @param g The green component of the opaque sRGB color to create, in \([0..1]\)
+ * @param b The blue component of the opaque sRGB color to create, in \([0..1]\)
+ * @return A non-null instance of {@link Color}
+ */
+ @NonNull
+ public static Color valueOf(float r, float g, float b) {
+ return new Color(r, g, b, 1.0f);
+ }
+
+ /**
+ * Creates a new <code>Color</code> in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space with the specified red, green, blue and alpha component values.
+ * The component values must be in the range \([0..1]\).
+ *
+ * @param r The red component of the sRGB color to create, in \([0..1]\)
+ * @param g The green component of the sRGB color to create, in \([0..1]\)
+ * @param b The blue component of the sRGB color to create, in \([0..1]\)
+ * @param a The alpha component of the sRGB color to create, in \([0..1]\)
+ * @return A non-null instance of {@link Color}
+ */
+ @NonNull
+ public static Color valueOf(float r, float g, float b, float a) {
+ return new Color(saturate(r), saturate(g), saturate(b), saturate(a));
+ }
+
+ /**
+ * Creates a new <code>Color</code> in the specified color space with the
+ * specified red, green, blue and alpha component values. The range of the
+ * components is defined by {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}. The values passed to this method
+ * must be in the proper range.
+ *
+ * @param r The red component of the color to create
+ * @param g The green component of the color to create
+ * @param b The blue component of the color to create
+ * @param a The alpha component of the color to create, in \([0..1]\)
+ * @param colorSpace The color space of the color to create
+ * @return A non-null instance of {@link Color}
+ *
+ * @throws IllegalArgumentException If the specified color space uses a
+ * color model with more than 3 components
+ */
+ @NonNull
+ public static Color valueOf(float r, float g, float b, float a, @NonNull ColorSpace colorSpace) {
+ if (colorSpace.getComponentCount() > 3) {
+ throw new IllegalArgumentException("The specified color space must use a color model " +
+ "with at most 3 color components");
+ }
+ return new Color(r, g, b, a, colorSpace);
+ }
+
+ /**
+ * <p>Creates a new <code>Color</code> in the specified color space with the
+ * specified component values. The range of the components is defined by
+ * {@link ColorSpace#getMinValue(int)} and {@link ColorSpace#getMaxValue(int)}.
+ * The values passed to this method must be in the proper range. The alpha
+ * component is always in the range \([0..1]\).</p>
+ *
+ * <p>The length of the array of components must be at least
+ * <code>{@link ColorSpace#getComponentCount()} + 1</code>. The component at index
+ * {@link ColorSpace#getComponentCount()} is always alpha.</p>
+ *
+ * @param components The components of the color to create, with alpha as the last component
+ * @param colorSpace The color space of the color to create
+ * @return A non-null instance of {@link Color}
+ *
+ * @throws IllegalArgumentException If the array of components is smaller than
+ * required by the color space
+ */
+ @NonNull
+ public static Color valueOf(@NonNull @Size(min = 4, max = 5) float[] components,
+ @NonNull ColorSpace colorSpace) {
+ if (components.length < colorSpace.getComponentCount() + 1) {
+ throw new IllegalArgumentException("Received a component array of length " +
+ components.length + " but the color model requires " +
+ (colorSpace.getComponentCount() + 1) + " (including alpha)");
+ }
+ return new Color(Arrays.copyOf(components, colorSpace.getComponentCount() + 1), colorSpace);
+ }
+
+ /**
+ * Converts the specified ARGB color int to an RGBA color long in the sRGB
+ * color space. See the documentation of this class for a description of
+ * the color long format.
+ *
+ * @param color The ARGB color int to convert to an RGBA color long in sRGB
+ *
+ * @return A color long
+ */
+ @ColorLong
+ public static long pack(@ColorInt int color) {
+ return (color & 0xffffffffL) << 32;
+ }
+
+ /**
+ * Packs the sRGB color defined by the specified red, green and blue component
+ * values into an RGBA color long in the sRGB color space. The alpha component
+ * is set to 1.0. See the documentation of this class for a description of the
+ * color long format.
+ *
+ * @param red The red component of the sRGB color to create, in \([0..1]\)
+ * @param green The green component of the sRGB color to create, in \([0..1]\)
+ * @param blue The blue component of the sRGB color to create, in \([0..1]\)
+ *
+ * @return A color long
+ */
+ @ColorLong
+ public static long pack(float red, float green, float blue) {
+ return pack(red, green, blue, 1.0f, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+
+ /**
+ * Packs the sRGB color defined by the specified red, green, blue and alpha
+ * component values into an RGBA color long in the sRGB color space. See the
+ * documentation of this class for a description of the color long format.
+ *
+ * @param red The red component of the sRGB color to create, in \([0..1]\)
+ * @param green The green component of the sRGB color to create, in \([0..1]\)
+ * @param blue The blue component of the sRGB color to create, in \([0..1]\)
+ * @param alpha The alpha component of the sRGB color to create, in \([0..1]\)
+ *
+ * @return A color long
+ */
+ @ColorLong
+ public static long pack(float red, float green, float blue, float alpha) {
+ return pack(red, green, blue, alpha, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+
+ /**
+ * <p>Packs the 3 component color defined by the specified red, green, blue and
+ * alpha component values into a color long in the specified color space. See the
+ * documentation of this class for a description of the color long format.</p>
+ *
+ * <p>The red, green and blue components must be in the range defined by the
+ * specified color space. See {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}.</p>
+ *
+ * @param red The red component of the color to create
+ * @param green The green component of the color to create
+ * @param blue The blue component of the color to create
+ * @param alpha The alpha component of the color to create, in \([0..1]\)
+ *
+ * @return A color long
+ *
+ * @throws IllegalArgumentException If the color space's id is {@link ColorSpace#MIN_ID}
+ * or if the color space's color model has more than 3 components
+ */
+ @ColorLong
+ public static long pack(float red, float green, float blue, float alpha,
+ @NonNull ColorSpace colorSpace) {
+ if (colorSpace.isSrgb()) {
+ int argb =
+ ((int) (alpha * 255.0f + 0.5f) << 24) |
+ ((int) (red * 255.0f + 0.5f) << 16) |
+ ((int) (green * 255.0f + 0.5f) << 8) |
+ (int) (blue * 255.0f + 0.5f);
+ return (argb & 0xffffffffL) << 32;
+ }
+
+ int id = colorSpace.getId();
+ if (id == ColorSpace.MIN_ID) {
+ throw new IllegalArgumentException(
+ "Unknown color space, please use a color space returned by ColorSpace.get()");
+ }
+ if (colorSpace.getComponentCount() > 3) {
+ throw new IllegalArgumentException(
+ "The color space must use a color model with at most 3 components");
+ }
+
+ @HalfFloat short r = Half.toHalf(red);
+ @HalfFloat short g = Half.toHalf(green);
+ @HalfFloat short b = Half.toHalf(blue);
+
+ int a = (int) (Math.max(0.0f, Math.min(alpha, 1.0f)) * 1023.0f + 0.5f);
+
+ // Suppress sign extension
+ return (r & 0xffffL) << 48 |
+ (g & 0xffffL) << 32 |
+ (b & 0xffffL) << 16 |
+ (a & 0x3ffL ) << 6 |
+ id & 0x3fL;
+ }
+
+ /**
+ * Converts the specified ARGB color int from the {@link ColorSpace.Named#SRGB sRGB}
+ * color space into the specified destination color space. The resulting color is
+ * returned as a color long. See the documentation of this class for a description
+ * of the color long format.
+ *
+ * @param color The sRGB color int to convert
+ * @param colorSpace The destination color space
+ * @return A color long in the destination color space
+ */
+ @ColorLong
+ public static long convert(@ColorInt int color, @NonNull ColorSpace colorSpace) {
+ float r = ((color >> 16) & 0xff) / 255.0f;
+ float g = ((color >> 8) & 0xff) / 255.0f;
+ float b = ((color ) & 0xff) / 255.0f;
+ float a = ((color >> 24) & 0xff) / 255.0f;
+ ColorSpace source = ColorSpace.get(ColorSpace.Named.SRGB);
+ return convert(r, g, b, a, source, colorSpace);
+ }
+
+ /**
+ * <p>Converts the specified color long from its color space into the specified
+ * destination color space. The resulting color is returned as a color long. See
+ * the documentation of this class for a description of the color long format.</p>
+ *
+ * <p>When converting several colors in a row, it is recommended to use
+ * {@link #convert(long, ColorSpace.Connector)} instead to
+ * avoid the creation of a {@link ColorSpace.Connector} on every invocation.</p>
+ *
+ * @param color The color long to convert
+ * @param colorSpace The destination color space
+ * @return A color long in the destination color space
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ */
+ @ColorLong
+ public static long convert(@ColorLong long color, @NonNull ColorSpace colorSpace) {
+ float r = red(color);
+ float g = green(color);
+ float b = blue(color);
+ float a = alpha(color);
+ ColorSpace source = colorSpace(color);
+ return convert(r, g, b, a, source, colorSpace);
+ }
+
+ /**
+ * <p>Converts the specified 3 component color from the source color space to the
+ * destination color space. The resulting color is returned as a color long. See
+ * the documentation of this class for a description of the color long format.</p>
+ *
+ * <p>When converting multiple colors in a row, it is recommended to use
+ * {@link #convert(float, float, float, float, ColorSpace.Connector)} instead to
+ * avoid the creation of a {@link ColorSpace.Connector} on every invocation.</p>
+ *
+ * <p>The red, green and blue components must be in the range defined by the
+ * specified color space. See {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}.</p>
+ *
+ * @param r The red component of the color to convert
+ * @param g The green component of the color to convert
+ * @param b The blue component of the color to convert
+ * @param a The alpha component of the color to convert, in \([0..1]\)
+ * @param source The source color space, cannot be null
+ * @param destination The destination color space, cannot be null
+ * @return A color long in the destination color space
+ *
+ * @see #convert(float, float, float, float, ColorSpace.Connector)
+ */
+ @ColorLong
+ public static long convert(float r, float g, float b, float a,
+ @NonNull ColorSpace source, @NonNull ColorSpace destination) {
+ float[] c = ColorSpace.connect(source, destination).transform(r, g, b);
+ return pack(c[0], c[1], c[2], a, destination);
+ }
+
+ /**
+ * <p>Converts the specified color long from a color space to another using the
+ * specified color space {@link ColorSpace.Connector connector}. The resulting
+ * color is returned as a color long. See the documentation of this class for a
+ * description of the color long format.</p>
+ *
+ * <p>When converting several colors in a row, this method is preferable to
+ * {@link #convert(long, ColorSpace)} as it prevents a new connector from being
+ * created on every invocation.</p>
+ *
+ * <p class="note">The connector's source color space should match the color long's
+ * color space.</p>
+ *
+ * @param color The color long to convert
+ * @param connector A color space connector, cannot be null
+ * @return A color long in the destination color space of the connector
+ */
+ @ColorLong
+ public static long convert(@ColorLong long color, @NonNull ColorSpace.Connector connector) {
+ float r = red(color);
+ float g = green(color);
+ float b = blue(color);
+ float a = alpha(color);
+ return convert(r, g, b, a, connector);
+ }
+
+ /**
+ * <p>Converts the specified 3 component color from a color space to another using
+ * the specified color space {@link ColorSpace.Connector connector}. The resulting
+ * color is returned as a color long. See the documentation of this class for a
+ * description of the color long format.</p>
+ *
+ * <p>When converting several colors in a row, this method is preferable to
+ * {@link #convert(float, float, float, float, ColorSpace, ColorSpace)} as
+ * it prevents a new connector from being created on every invocation.</p>
+ *
+ * <p>The red, green and blue components must be in the range defined by the
+ * source color space of the connector. See {@link ColorSpace#getMinValue(int)}
+ * and {@link ColorSpace#getMaxValue(int)}.</p>
+ *
+ * @param r The red component of the color to convert
+ * @param g The green component of the color to convert
+ * @param b The blue component of the color to convert
+ * @param a The alpha component of the color to convert, in \([0..1]\)
+ * @param connector A color space connector, cannot be null
+ * @return A color long in the destination color space of the connector
+ *
+ * @see #convert(float, float, float, float, ColorSpace, ColorSpace)
+ */
+ @ColorLong
+ public static long convert(float r, float g, float b, float a,
+ @NonNull ColorSpace.Connector connector) {
+ float[] c = connector.transform(r, g, b);
+ return pack(c[0], c[1], c[2], a, connector.getDestination());
+ }
+
+ /**
+ * <p>Returns the relative luminance of a color.</p>
+ *
+ * <p>Based on the formula for relative luminance defined in WCAG 2.0,
+ * W3C Recommendation 11 December 2008.</p>
+ *
+ * @return A value between 0 (darkest black) and 1 (lightest white)
+ *
+ * @throws IllegalArgumentException If the specified color's color space
+ * is unknown or does not use the {@link ColorSpace.Model#RGB RGB} color model
+ */
+ public static float luminance(@ColorLong long color) {
+ ColorSpace colorSpace = colorSpace(color);
+ if (colorSpace.getModel() != ColorSpace.Model.RGB) {
+ throw new IllegalArgumentException("The specified color must be encoded in an RGB " +
+ "color space. The supplied color space is " + colorSpace.getModel());
+ }
+
+ DoubleUnaryOperator eotf = ((ColorSpace.Rgb) colorSpace).getEotf();
+ double r = eotf.applyAsDouble(red(color));
+ double g = eotf.applyAsDouble(green(color));
+ double b = eotf.applyAsDouble(blue(color));
+
+ return saturate((float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)));
+ }
+
+ private static float saturate(float v) {
+ return v <= 0.0f ? 0.0f : (v >= 1.0f ? 1.0f : v);
+ }
+
+ /**
+ * Return the alpha component of a color int. This is the same as saying
+ * color >>> 24
+ */
+ @IntRange(from = 0, to = 255)
+ public static int alpha(int color) {
+ return color >>> 24;
+ }
+
+ /**
+ * Return the red component of a color int. This is the same as saying
+ * (color >> 16) & 0xFF
+ */
+ @IntRange(from = 0, to = 255)
+ public static int red(int color) {
+ return (color >> 16) & 0xFF;
+ }
+
+ /**
+ * Return the green component of a color int. This is the same as saying
+ * (color >> 8) & 0xFF
+ */
+ @IntRange(from = 0, to = 255)
+ public static int green(int color) {
+ return (color >> 8) & 0xFF;
+ }
+
+ /**
+ * Return the blue component of a color int. This is the same as saying
+ * color & 0xFF
+ */
+ @IntRange(from = 0, to = 255)
+ public static int blue(int color) {
+ return color & 0xFF;
+ }
+
+ /**
+ * Return a color-int from red, green, blue components.
+ * The alpha component is implicitly 255 (fully opaque).
+ * These component values should be \([0..255]\), but there is no
+ * range check performed, so if they are out of range, the
+ * returned color is undefined.
+ *
+ * @param red Red component \([0..255]\) of the color
+ * @param green Green component \([0..255]\) of the color
+ * @param blue Blue component \([0..255]\) of the color
+ */
+ @ColorInt
+ public static int rgb(
+ @IntRange(from = 0, to = 255) int red,
+ @IntRange(from = 0, to = 255) int green,
+ @IntRange(from = 0, to = 255) int blue) {
+ return 0xff000000 | (red << 16) | (green << 8) | blue;
+ }
+
+ /**
+ * Return a color-int from red, green, blue float components
+ * in the range \([0..1]\). The alpha component is implicitly
+ * 1.0 (fully opaque). If the components are out of range, the
+ * returned color is undefined.
+ *
+ * @param red Red component \([0..1]\) of the color
+ * @param green Green component \([0..1]\) of the color
+ * @param blue Blue component \([0..1]\) of the color
+ */
+ @ColorInt
+ public static int rgb(float red, float green, float blue) {
+ return 0xff000000 |
+ ((int) (red * 255.0f + 0.5f) << 16) |
+ ((int) (green * 255.0f + 0.5f) << 8) |
+ (int) (blue * 255.0f + 0.5f);
+ }
+
+ /**
+ * Return a color-int from alpha, red, green, blue components.
+ * These component values should be \([0..255]\), but there is no
+ * range check performed, so if they are out of range, the
+ * returned color is undefined.
+ * @param alpha Alpha component \([0..255]\) of the color
+ * @param red Red component \([0..255]\) of the color
+ * @param green Green component \([0..255]\) of the color
+ * @param blue Blue component \([0..255]\) of the color
+ */
+ @ColorInt
+ public static int argb(
+ @IntRange(from = 0, to = 255) int alpha,
+ @IntRange(from = 0, to = 255) int red,
+ @IntRange(from = 0, to = 255) int green,
+ @IntRange(from = 0, to = 255) int blue) {
+ return (alpha << 24) | (red << 16) | (green << 8) | blue;
+ }
+
+ /**
+ * Return a color-int from alpha, red, green, blue float components
+ * in the range \([0..1]\). If the components are out of range, the
+ * returned color is undefined.
+ *
+ * @param alpha Alpha component \([0..1]\) of the color
+ * @param red Red component \([0..1]\) of the color
+ * @param green Green component \([0..1]\) of the color
+ * @param blue Blue component \([0..1]\) of the color
+ */
+ @ColorInt
+ public static int argb(float alpha, float red, float green, float blue) {
+ return ((int) (alpha * 255.0f + 0.5f) << 24) |
+ ((int) (red * 255.0f + 0.5f) << 16) |
+ ((int) (green * 255.0f + 0.5f) << 8) |
+ (int) (blue * 255.0f + 0.5f);
+ }
+
+ /**
+ * Returns the relative luminance of a color.
+ * <p>
+ * Assumes sRGB encoding. Based on the formula for relative luminance
+ * defined in WCAG 2.0, W3C Recommendation 11 December 2008.
+ *
+ * @return a value between 0 (darkest black) and 1 (lightest white)
+ */
+ public static float luminance(@ColorInt int color) {
+ ColorSpace.Rgb cs = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+ DoubleUnaryOperator eotf = cs.getEotf();
+
+ double r = eotf.applyAsDouble(red(color) / 255.0);
+ double g = eotf.applyAsDouble(green(color) / 255.0);
+ double b = eotf.applyAsDouble(blue(color) / 255.0);
+
+ return (float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b));
+ }
+
+ /**
+ * </p>Parse the color string, and return the corresponding color-int.
+ * If the string cannot be parsed, throws an IllegalArgumentException
+ * exception. Supported formats are:</p>
+ *
+ * <ul>
+ * <li><code>#RRGGBB</code></li>
+ * <li><code>#AARRGGBB</code></li>
+ * </ul>
+ *
+ * <p>The following names are also accepted: <code>red</code>, <code>blue</code>,
+ * <code>green</code>, <code>black</code>, <code>white</code>, <code>gray</code>,
+ * <code>cyan</code>, <code>magenta</code>, <code>yellow</code>, <code>lightgray</code>,
+ * <code>darkgray</code>, <code>grey</code>, <code>lightgrey</code>, <code>darkgrey</code>,
+ * <code>aqua</code>, <code>fuchsia</code>, <code>lime</code>, <code>maroon</code>,
+ * <code>navy</code>, <code>olive</code>, <code>purple</code>, <code>silver</code>,
+ * and <code>teal</code>.</p>
+ */
+ @ColorInt
+ public static int parseColor(@Size(min=1) String colorString) {
+ if (colorString.charAt(0) == '#') {
+ // Use a long to avoid rollovers on #ffXXXXXX
+ long color = Long.parseLong(colorString.substring(1), 16);
+ if (colorString.length() == 7) {
+ // Set the alpha value
+ color |= 0x00000000ff000000;
+ } else if (colorString.length() != 9) {
+ throw new IllegalArgumentException("Unknown color");
+ }
+ return (int)color;
+ } else {
+ Integer color = sColorNameMap.get(colorString.toLowerCase(Locale.ROOT));
+ if (color != null) {
+ return color;
+ }
+ }
+ throw new IllegalArgumentException("Unknown color");
+ }
+
+ /**
+ * Convert RGB components to HSV.
+ * <ul>
+ * <li><code>hsv[0]</code> is Hue \([0..360[\)</li>
+ * <li><code>hsv[1]</code> is Saturation \([0...1]\)</li>
+ * <li><code>hsv[2]</code> is Value \([0...1]\)</li>
+ * </ul>
+ * @param red red component value \([0..255]\)
+ * @param green green component value \([0..255]\)
+ * @param blue blue component value \([0..255]\)
+ * @param hsv 3 element array which holds the resulting HSV components.
+ */
+ public static void RGBToHSV(
+ @IntRange(from = 0, to = 255) int red,
+ @IntRange(from = 0, to = 255) int green,
+ @IntRange(from = 0, to = 255) int blue, @Size(3) float hsv[]) {
+ if (hsv.length < 3) {
+ throw new RuntimeException("3 components required for hsv");
+ }
+ nativeRGBToHSV(red, green, blue, hsv);
+ }
+
+ /**
+ * Convert the ARGB color to its HSV components.
+ * <ul>
+ * <li><code>hsv[0]</code> is Hue \([0..360[\)</li>
+ * <li><code>hsv[1]</code> is Saturation \([0...1]\)</li>
+ * <li><code>hsv[2]</code> is Value \([0...1]\)</li>
+ * </ul>
+ * @param color the argb color to convert. The alpha component is ignored.
+ * @param hsv 3 element array which holds the resulting HSV components.
+ */
+ public static void colorToHSV(@ColorInt int color, @Size(3) float hsv[]) {
+ RGBToHSV((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, hsv);
+ }
+
+ /**
+ * Convert HSV components to an ARGB color. Alpha set to 0xFF.
+ * <ul>
+ * <li><code>hsv[0]</code> is Hue \([0..360[\)</li>
+ * <li><code>hsv[1]</code> is Saturation \([0...1]\)</li>
+ * <li><code>hsv[2]</code> is Value \([0...1]\)</li>
+ * </ul>
+ * If hsv values are out of range, they are pinned.
+ * @param hsv 3 element array which holds the input HSV components.
+ * @return the resulting argb color
+ */
+ @ColorInt
+ public static int HSVToColor(@Size(3) float hsv[]) {
+ return HSVToColor(0xFF, hsv);
+ }
+
+ /**
+ * Convert HSV components to an ARGB color. The alpha component is passed
+ * through unchanged.
+ * <ul>
+ * <li><code>hsv[0]</code> is Hue \([0..360[\)</li>
+ * <li><code>hsv[1]</code> is Saturation \([0...1]\)</li>
+ * <li><code>hsv[2]</code> is Value \([0...1]\)</li>
+ * </ul>
+ * If hsv values are out of range, they are pinned.
+ * @param alpha the alpha component of the returned argb color.
+ * @param hsv 3 element array which holds the input HSV components.
+ * @return the resulting argb color
+ */
+ @ColorInt
+ public static int HSVToColor(@IntRange(from = 0, to = 255) int alpha, @Size(3) float hsv[]) {
+ if (hsv.length < 3) {
+ throw new RuntimeException("3 components required for hsv");
+ }
+ return nativeHSVToColor(alpha, hsv);
+ }
+
+ private static native void nativeRGBToHSV(int red, int greed, int blue, float hsv[]);
+ private static native int nativeHSVToColor(int alpha, float hsv[]);
+
+ /**
+ * Converts an HTML color (named or numeric) to an integer RGB value.
+ *
+ * @param color Non-null color string.
+ *
+ * @return A color value, or {@code -1} if the color string could not be interpreted.
+ *
+ * @hide
+ */
+ @ColorInt
+ public static int getHtmlColor(@NonNull String color) {
+ Integer i = sColorNameMap.get(color.toLowerCase(Locale.ROOT));
+ if (i != null) {
+ return i;
+ } else {
+ try {
+ return XmlUtils.convertValueToInt(color, -1);
+ } catch (NumberFormatException nfe) {
+ return -1;
+ }
+ }
+ }
+
+ private static final HashMap<String, Integer> sColorNameMap;
+ static {
+ sColorNameMap = new HashMap<>();
+ sColorNameMap.put("black", BLACK);
+ sColorNameMap.put("darkgray", DKGRAY);
+ sColorNameMap.put("gray", GRAY);
+ sColorNameMap.put("lightgray", LTGRAY);
+ sColorNameMap.put("white", WHITE);
+ sColorNameMap.put("red", RED);
+ sColorNameMap.put("green", GREEN);
+ sColorNameMap.put("blue", BLUE);
+ sColorNameMap.put("yellow", YELLOW);
+ sColorNameMap.put("cyan", CYAN);
+ sColorNameMap.put("magenta", MAGENTA);
+ sColorNameMap.put("aqua", 0xFF00FFFF);
+ sColorNameMap.put("fuchsia", 0xFFFF00FF);
+ sColorNameMap.put("darkgrey", DKGRAY);
+ sColorNameMap.put("grey", GRAY);
+ sColorNameMap.put("lightgrey", LTGRAY);
+ sColorNameMap.put("lime", 0xFF00FF00);
+ sColorNameMap.put("maroon", 0xFF800000);
+ sColorNameMap.put("navy", 0xFF000080);
+ sColorNameMap.put("olive", 0xFF808000);
+ sColorNameMap.put("purple", 0xFF800080);
+ sColorNameMap.put("silver", 0xFFC0C0C0);
+ sColorNameMap.put("teal", 0xFF008080);
+
+ }
+}
diff --git a/android/graphics/ColorFilter.java b/android/graphics/ColorFilter.java
new file mode 100644
index 0000000..b24b988
--- /dev/null
+++ b/android/graphics/ColorFilter.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import libcore.util.NativeAllocationRegistry;
+
+/**
+ * A color filter can be used with a {@link Paint} to modify the color of
+ * each pixel drawn with that paint. This is an abstract class that should
+ * never be used directly.
+ */
+public class ColorFilter {
+
+ private static class NoImagePreloadHolder {
+ public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
+ ColorFilter.class.getClassLoader(), nativeGetFinalizer(), 50);
+ }
+
+ /**
+ * @deprecated Use subclass constructors directly instead.
+ */
+ @Deprecated
+ public ColorFilter() {}
+
+ /**
+ * Current native SkColorFilter instance.
+ */
+ private long mNativeInstance;
+ // Runnable to do immediate destruction
+ private Runnable mCleaner;
+
+ long createNativeInstance() {
+ return 0;
+ }
+
+ void discardNativeInstance() {
+ if (mNativeInstance != 0) {
+ mCleaner.run();
+ mCleaner = null;
+ mNativeInstance = 0;
+ }
+ }
+
+ /** @hide */
+ public long getNativeInstance() {
+ if (mNativeInstance == 0) {
+ mNativeInstance = createNativeInstance();
+
+ if (mNativeInstance != 0) {
+ // Note: we must check for null here, since it's possible for createNativeInstance()
+ // to return nullptr if the native SkColorFilter would be a no-op at draw time.
+ // See native implementations of subclass create methods for more info.
+ mCleaner = NoImagePreloadHolder.sRegistry.registerNativeAllocation(
+ this, mNativeInstance);
+ }
+ }
+ return mNativeInstance;
+
+ }
+
+ private static native long nativeGetFinalizer();
+}
diff --git a/android/graphics/ColorFilter_Delegate.java b/android/graphics/ColorFilter_Delegate.java
new file mode 100644
index 0000000..84424bc
--- /dev/null
+++ b/android/graphics/ColorFilter_Delegate.java
@@ -0,0 +1,83 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.awt.Graphics2D;
+
+import libcore.util.NativeAllocationRegistry_Delegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.ColorFilter
+ *
+ * Through the layoutlib_create tool, the original native methods of ColorFilter have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original ColorFilter class.
+ *
+ * This also serve as a base class for all ColorFilter delegate classes.
+ *
+ * @see DelegateManager
+ *
+ */
+public abstract class ColorFilter_Delegate {
+
+ // ---- delegate manager ----
+ protected static final DelegateManager<ColorFilter_Delegate> sManager =
+ new DelegateManager<ColorFilter_Delegate>(ColorFilter_Delegate.class);
+ private static long sFinalizer = -1;
+
+ // ---- delegate helper data ----
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ public static ColorFilter_Delegate getDelegate(long nativeShader) {
+ return sManager.getDelegate(nativeShader);
+ }
+
+ public abstract String getSupportMessage();
+
+ public boolean isSupported() {
+ return false;
+ }
+
+ public void applyFilter(Graphics2D g, int width, int height) {
+ // This should never be called directly. If supported, the sub class should override this.
+ assert false;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeGetFinalizer() {
+ synchronized (ColorFilter_Delegate.class) {
+ if (sFinalizer == -1) {
+ sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
+ sManager::removeJavaReferenceFor);
+ }
+ }
+ return sFinalizer;
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/ColorMatrix.java b/android/graphics/ColorMatrix.java
new file mode 100644
index 0000000..6299b2c
--- /dev/null
+++ b/android/graphics/ColorMatrix.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import java.util.Arrays;
+
+/**
+ * 4x5 matrix for transforming the color and alpha components of a Bitmap.
+ * The matrix can be passed as single array, and is treated as follows:
+ *
+ * <pre>
+ * [ a, b, c, d, e,
+ * f, g, h, i, j,
+ * k, l, m, n, o,
+ * p, q, r, s, t ]</pre>
+ *
+ * <p>
+ * When applied to a color <code>[R, G, B, A]</code>, the resulting color
+ * is computed as:
+ * </p>
+ *
+ * <pre>
+ * R’ = a*R + b*G + c*B + d*A + e;
+ * G’ = f*R + g*G + h*B + i*A + j;
+ * B’ = k*R + l*G + m*B + n*A + o;
+ * A’ = p*R + q*G + r*B + s*A + t;</pre>
+ *
+ * <p>
+ * That resulting color <code>[R’, G’, B’, A’]</code>
+ * then has each channel clamped to the <code>0</code> to <code>255</code>
+ * range.
+ * </p>
+ *
+ * <p>
+ * The sample ColorMatrix below inverts incoming colors by scaling each
+ * channel by <code>-1</code>, and then shifting the result up by
+ * <code>255</code> to remain in the standard color space.
+ * </p>
+ *
+ * <pre>
+ * [ -1, 0, 0, 0, 255,
+ * 0, -1, 0, 0, 255,
+ * 0, 0, -1, 0, 255,
+ * 0, 0, 0, 1, 0 ]</pre>
+ */
+@SuppressWarnings({ "MismatchedReadAndWriteOfArray", "PointlessArithmeticExpression" })
+public class ColorMatrix {
+ private final float[] mArray = new float[20];
+
+ /**
+ * Create a new colormatrix initialized to identity (as if reset() had
+ * been called).
+ */
+ public ColorMatrix() {
+ reset();
+ }
+
+ /**
+ * Create a new colormatrix initialized with the specified array of values.
+ */
+ public ColorMatrix(float[] src) {
+ System.arraycopy(src, 0, mArray, 0, 20);
+ }
+
+ /**
+ * Create a new colormatrix initialized with the specified colormatrix.
+ */
+ public ColorMatrix(ColorMatrix src) {
+ System.arraycopy(src.mArray, 0, mArray, 0, 20);
+ }
+
+ /**
+ * Return the array of floats representing this colormatrix.
+ */
+ public final float[] getArray() { return mArray; }
+
+ /**
+ * Set this colormatrix to identity:
+ * <pre>
+ * [ 1 0 0 0 0 - red vector
+ * 0 1 0 0 0 - green vector
+ * 0 0 1 0 0 - blue vector
+ * 0 0 0 1 0 ] - alpha vector
+ * </pre>
+ */
+ public void reset() {
+ final float[] a = mArray;
+ Arrays.fill(a, 0);
+ a[0] = a[6] = a[12] = a[18] = 1;
+ }
+
+ /**
+ * Assign the src colormatrix into this matrix, copying all of its values.
+ */
+ public void set(ColorMatrix src) {
+ System.arraycopy(src.mArray, 0, mArray, 0, 20);
+ }
+
+ /**
+ * Assign the array of floats into this matrix, copying all of its values.
+ */
+ public void set(float[] src) {
+ System.arraycopy(src, 0, mArray, 0, 20);
+ }
+
+ /**
+ * Set this colormatrix to scale by the specified values.
+ */
+ public void setScale(float rScale, float gScale, float bScale,
+ float aScale) {
+ final float[] a = mArray;
+
+ for (int i = 19; i > 0; --i) {
+ a[i] = 0;
+ }
+ a[0] = rScale;
+ a[6] = gScale;
+ a[12] = bScale;
+ a[18] = aScale;
+ }
+
+ /**
+ * Set the rotation on a color axis by the specified values.
+ * <p>
+ * <code>axis=0</code> correspond to a rotation around the RED color
+ * <code>axis=1</code> correspond to a rotation around the GREEN color
+ * <code>axis=2</code> correspond to a rotation around the BLUE color
+ * </p>
+ */
+ public void setRotate(int axis, float degrees) {
+ reset();
+ double radians = degrees * Math.PI / 180d;
+ float cosine = (float) Math.cos(radians);
+ float sine = (float) Math.sin(radians);
+ switch (axis) {
+ // Rotation around the red color
+ case 0:
+ mArray[6] = mArray[12] = cosine;
+ mArray[7] = sine;
+ mArray[11] = -sine;
+ break;
+ // Rotation around the green color
+ case 1:
+ mArray[0] = mArray[12] = cosine;
+ mArray[2] = -sine;
+ mArray[10] = sine;
+ break;
+ // Rotation around the blue color
+ case 2:
+ mArray[0] = mArray[6] = cosine;
+ mArray[1] = sine;
+ mArray[5] = -sine;
+ break;
+ default:
+ throw new RuntimeException();
+ }
+ }
+
+ /**
+ * Set this colormatrix to the concatenation of the two specified
+ * colormatrices, such that the resulting colormatrix has the same effect
+ * as applying matB and then applying matA.
+ * <p>
+ * It is legal for either matA or matB to be the same colormatrix as this.
+ * </p>
+ */
+ public void setConcat(ColorMatrix matA, ColorMatrix matB) {
+ float[] tmp;
+ if (matA == this || matB == this) {
+ tmp = new float[20];
+ } else {
+ tmp = mArray;
+ }
+
+ final float[] a = matA.mArray;
+ final float[] b = matB.mArray;
+ int index = 0;
+ for (int j = 0; j < 20; j += 5) {
+ for (int i = 0; i < 4; i++) {
+ tmp[index++] = a[j + 0] * b[i + 0] + a[j + 1] * b[i + 5] +
+ a[j + 2] * b[i + 10] + a[j + 3] * b[i + 15];
+ }
+ tmp[index++] = a[j + 0] * b[4] + a[j + 1] * b[9] +
+ a[j + 2] * b[14] + a[j + 3] * b[19] +
+ a[j + 4];
+ }
+
+ if (tmp != mArray) {
+ System.arraycopy(tmp, 0, mArray, 0, 20);
+ }
+ }
+
+ /**
+ * Concat this colormatrix with the specified prematrix.
+ * <p>
+ * This is logically the same as calling setConcat(this, prematrix);
+ * </p>
+ */
+ public void preConcat(ColorMatrix prematrix) {
+ setConcat(this, prematrix);
+ }
+
+ /**
+ * Concat this colormatrix with the specified postmatrix.
+ * <p>
+ * This is logically the same as calling setConcat(postmatrix, this);
+ * </p>
+ */
+ public void postConcat(ColorMatrix postmatrix) {
+ setConcat(postmatrix, this);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Set the matrix to affect the saturation of colors.
+ *
+ * @param sat A value of 0 maps the color to gray-scale. 1 is identity.
+ */
+ public void setSaturation(float sat) {
+ reset();
+ float[] m = mArray;
+
+ final float invSat = 1 - sat;
+ final float R = 0.213f * invSat;
+ final float G = 0.715f * invSat;
+ final float B = 0.072f * invSat;
+
+ m[0] = R + sat; m[1] = G; m[2] = B;
+ m[5] = R; m[6] = G + sat; m[7] = B;
+ m[10] = R; m[11] = G; m[12] = B + sat;
+ }
+
+ /**
+ * Set the matrix to convert RGB to YUV
+ */
+ public void setRGB2YUV() {
+ reset();
+ float[] m = mArray;
+ // these coefficients match those in libjpeg
+ m[0] = 0.299f; m[1] = 0.587f; m[2] = 0.114f;
+ m[5] = -0.16874f; m[6] = -0.33126f; m[7] = 0.5f;
+ m[10] = 0.5f; m[11] = -0.41869f; m[12] = -0.08131f;
+ }
+
+ /**
+ * Set the matrix to convert from YUV to RGB
+ */
+ public void setYUV2RGB() {
+ reset();
+ float[] m = mArray;
+ // these coefficients match those in libjpeg
+ m[2] = 1.402f;
+ m[5] = 1; m[6] = -0.34414f; m[7] = -0.71414f;
+ m[10] = 1; m[11] = 1.772f; m[12] = 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // if (obj == this) return true; -- NaN value would mean matrix != itself
+ if (!(obj instanceof ColorMatrix)) {
+ return false;
+ }
+
+ // we don't use Arrays.equals(), since that considers NaN == NaN
+ final float[] other = ((ColorMatrix) obj).mArray;
+ for (int i = 0; i < 20; i++) {
+ if (other[i] != mArray[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/android/graphics/ColorMatrixColorFilter.java b/android/graphics/ColorMatrixColorFilter.java
new file mode 100644
index 0000000..9201a2e
--- /dev/null
+++ b/android/graphics/ColorMatrixColorFilter.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * A color filter that transforms colors through a 4x5 color matrix. This filter
+ * can be used to change the saturation of pixels, convert from YUV to RGB, etc.
+ *
+ * @see ColorMatrix
+ */
+public class ColorMatrixColorFilter extends ColorFilter {
+ private final ColorMatrix mMatrix = new ColorMatrix();
+
+ /**
+ * Create a color filter that transforms colors through a 4x5 color matrix.
+ *
+ * @param matrix 4x5 matrix used to transform colors. It is copied into
+ * the filter, so changes made to the matrix after the filter
+ * is constructed will not be reflected in the filter.
+ */
+ public ColorMatrixColorFilter(@NonNull ColorMatrix matrix) {
+ mMatrix.set(matrix);
+ }
+
+ /**
+ * Create a color filter that transforms colors through a 4x5 color matrix.
+ *
+ * @param array Array of floats used to transform colors, treated as a 4x5
+ * matrix. The first 20 entries of the array are copied into
+ * the filter. See ColorMatrix.
+ */
+ public ColorMatrixColorFilter(@NonNull float[] array) {
+ if (array.length < 20) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ mMatrix.set(array);
+ }
+
+ /**
+ * Copies the ColorMatrix from the filter into the passed ColorMatrix.
+ *
+ * @param colorMatrix Set to the current value of the filter's ColorMatrix.
+ */
+ public void getColorMatrix(ColorMatrix colorMatrix) {
+ colorMatrix.set(mMatrix);
+ }
+
+ /**
+ * Copies the provided color matrix to be used by this filter.
+ *
+ * If the specified color matrix is null, this filter's color matrix will be reset to the
+ * identity matrix.
+ *
+ * @param matrix A {@link ColorMatrix} or null
+ *
+ * @see #getColorMatrix(ColorMatrix)
+ * @see #setColorMatrixArray(float[])
+ * @see ColorMatrix#reset()
+ *
+ * @hide
+ */
+ public void setColorMatrix(@Nullable ColorMatrix matrix) {
+ discardNativeInstance();
+ if (matrix == null) {
+ mMatrix.reset();
+ } else {
+ mMatrix.set(matrix);
+ }
+ }
+
+ /**
+ * Copies the provided color matrix to be used by this filter.
+ *
+ * If the specified color matrix is null, this filter's color matrix will be reset to the
+ * identity matrix.
+ *
+ * @param array Array of floats used to transform colors, treated as a 4x5
+ * matrix. The first 20 entries of the array are copied into
+ * the filter. See {@link ColorMatrix}.
+ *
+ * @see #getColorMatrix(ColorMatrix)
+ * @see #setColorMatrix(ColorMatrix)
+ * @see ColorMatrix#reset()
+ *
+ * @throws ArrayIndexOutOfBoundsException if the specified array's
+ * length is < 20
+ *
+ * @hide
+ */
+ public void setColorMatrixArray(@Nullable float[] array) {
+ // called '...Array' so that passing null isn't ambiguous
+ discardNativeInstance();
+ if (array == null) {
+ mMatrix.reset();
+ } else {
+ if (array.length < 20) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ mMatrix.set(array);
+ }
+ }
+
+ @Override
+ long createNativeInstance() {
+ return nativeColorMatrixFilter(mMatrix.getArray());
+ }
+
+ private static native long nativeColorMatrixFilter(float[] array);
+}
diff --git a/android/graphics/ColorMatrixColorFilter_Delegate.java b/android/graphics/ColorMatrixColorFilter_Delegate.java
new file mode 100644
index 0000000..6739484
--- /dev/null
+++ b/android/graphics/ColorMatrixColorFilter_Delegate.java
@@ -0,0 +1,59 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.ColorMatrixColorFilter
+ *
+ * Through the layoutlib_create tool, the original native methods of ColorMatrixColorFilter have
+ * been replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original ColorMatrixColorFilter class.
+ *
+ * Because this extends {@link ColorFilter_Delegate}, there's no need to use a
+ * {@link DelegateManager}, as all the Shader classes will be added to the manager
+ * owned by {@link ColorFilter_Delegate}.
+ *
+ * @see ColorFilter_Delegate
+ *
+ */
+public class ColorMatrixColorFilter_Delegate extends ColorFilter_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public String getSupportMessage() {
+ return "ColorMatrix Color Filters are not supported.";
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeColorMatrixFilter(float[] array) {
+ ColorMatrixColorFilter_Delegate newDelegate = new ColorMatrixColorFilter_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/ColorSpace.java b/android/graphics/ColorSpace.java
new file mode 100644
index 0000000..5814df5
--- /dev/null
+++ b/android/graphics/ColorSpace.java
@@ -0,0 +1,4460 @@
+/*
+ * Copyright (C) 2016 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 android.graphics;
+
+import android.annotation.AnyThread;
+import android.annotation.ColorInt;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.SuppressAutoDoc;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.DoubleUnaryOperator;
+
+/**
+ * {@usesMathJax}
+ *
+ * <p>A {@link ColorSpace} is used to identify a specific organization of colors.
+ * Each color space is characterized by a {@link Model color model} that defines
+ * how a color value is represented (for instance the {@link Model#RGB RGB} color
+ * model defines a color value as a triplet of numbers).</p>
+ *
+ * <p>Each component of a color must fall within a valid range, specific to each
+ * color space, defined by {@link #getMinValue(int)} and {@link #getMaxValue(int)}
+ * This range is commonly \([0..1]\). While it is recommended to use values in the
+ * valid range, a color space always clamps input and output values when performing
+ * operations such as converting to a different color space.</p>
+ *
+ * <h3>Using color spaces</h3>
+ *
+ * <p>This implementation provides a pre-defined set of common color spaces
+ * described in the {@link Named} enum. To obtain an instance of one of the
+ * pre-defined color spaces, simply invoke {@link #get(Named)}:</p>
+ *
+ * <pre class="prettyprint">
+ * ColorSpace sRgb = ColorSpace.get(ColorSpace.Named.SRGB);
+ * </pre>
+ *
+ * <p>The {@link #get(Named)} method always returns the same instance for a given
+ * name. Color spaces with an {@link Model#RGB RGB} color model can be safely
+ * cast to {@link Rgb}. Doing so gives you access to more APIs to query various
+ * properties of RGB color models: color gamut primaries, transfer functions,
+ * conversions to and from linear space, etc. Please refer to {@link Rgb} for
+ * more information.</p>
+ *
+ * <p>The documentation of {@link Named} provides a detailed description of the
+ * various characteristics of each available color space.</p>
+ *
+ * <h3>Color space conversions</h3>
+
+ * <p>To allow conversion between color spaces, this implementation uses the CIE
+ * XYZ profile connection space (PCS). Color values can be converted to and from
+ * this PCS using {@link #toXyz(float[])} and {@link #fromXyz(float[])}.</p>
+ *
+ * <p>For color space with a non-RGB color model, the white point of the PCS
+ * <em>must be</em> the CIE standard illuminant D50. RGB color spaces use their
+ * native white point (D65 for {@link Named#SRGB sRGB} for instance and must
+ * undergo {@link Adaptation chromatic adaptation} as necessary.</p>
+ *
+ * <p>Since the white point of the PCS is not defined for RGB color space, it is
+ * highly recommended to use the variants of the {@link #connect(ColorSpace, ColorSpace)}
+ * method to perform conversions between color spaces. A color space can be
+ * manually adapted to a specific white point using {@link #adapt(ColorSpace, float[])}.
+ * Please refer to the documentation of {@link Rgb RGB color spaces} for more
+ * information. Several common CIE standard illuminants are provided in this
+ * class as reference (see {@link #ILLUMINANT_D65} or {@link #ILLUMINANT_D50}
+ * for instance).</p>
+ *
+ * <p>Here is an example of how to convert from a color space to another:</p>
+ *
+ * <pre class="prettyprint">
+ * // Convert from DCI-P3 to Rec.2020
+ * ColorSpace.Connector connector = ColorSpace.connect(
+ * ColorSpace.get(ColorSpace.Named.DCI_P3),
+ * ColorSpace.get(ColorSpace.Named.BT2020));
+ *
+ * float[] bt2020 = connector.transform(p3r, p3g, p3b);
+ * </pre>
+ *
+ * <p>You can easily convert to {@link Named#SRGB sRGB} by omitting the second
+ * parameter:</p>
+ *
+ * <pre class="prettyprint">
+ * // Convert from DCI-P3 to sRGB
+ * ColorSpace.Connector connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.DCI_P3));
+ *
+ * float[] sRGB = connector.transform(p3r, p3g, p3b);
+ * </pre>
+ *
+ * <p>Conversions also work between color spaces with different color models:</p>
+ *
+ * <pre class="prettyprint">
+ * // Convert from CIE L*a*b* (color model Lab) to Rec.709 (color model RGB)
+ * ColorSpace.Connector connector = ColorSpace.connect(
+ * ColorSpace.get(ColorSpace.Named.CIE_LAB),
+ * ColorSpace.get(ColorSpace.Named.BT709));
+ * </pre>
+ *
+ * <h3>Color spaces and multi-threading</h3>
+ *
+ * <p>Color spaces and other related classes ({@link Connector} for instance)
+ * are immutable and stateless. They can be safely used from multiple concurrent
+ * threads.</p>
+ *
+ * <p>Public static methods provided by this class, such as {@link #get(Named)}
+ * and {@link #connect(ColorSpace, ColorSpace)}, are also guaranteed to be
+ * thread-safe.</p>
+ *
+ * @see #get(Named)
+ * @see Named
+ * @see Model
+ * @see Connector
+ * @see Adaptation
+ */
+@AnyThread
+@SuppressWarnings("StaticInitializerReferencesSubClass")
+@SuppressAutoDoc
+public abstract class ColorSpace {
+ /**
+ * Standard CIE 1931 2° illuminant A, encoded in xyY.
+ * This illuminant has a color temperature of 2856K.
+ */
+ public static final float[] ILLUMINANT_A = { 0.44757f, 0.40745f };
+ /**
+ * Standard CIE 1931 2° illuminant B, encoded in xyY.
+ * This illuminant has a color temperature of 4874K.
+ */
+ public static final float[] ILLUMINANT_B = { 0.34842f, 0.35161f };
+ /**
+ * Standard CIE 1931 2° illuminant C, encoded in xyY.
+ * This illuminant has a color temperature of 6774K.
+ */
+ public static final float[] ILLUMINANT_C = { 0.31006f, 0.31616f };
+ /**
+ * Standard CIE 1931 2° illuminant D50, encoded in xyY.
+ * This illuminant has a color temperature of 5003K. This illuminant
+ * is used by the profile connection space in ICC profiles.
+ */
+ public static final float[] ILLUMINANT_D50 = { 0.34567f, 0.35850f };
+ /**
+ * Standard CIE 1931 2° illuminant D55, encoded in xyY.
+ * This illuminant has a color temperature of 5503K.
+ */
+ public static final float[] ILLUMINANT_D55 = { 0.33242f, 0.34743f };
+ /**
+ * Standard CIE 1931 2° illuminant D60, encoded in xyY.
+ * This illuminant has a color temperature of 6004K.
+ */
+ public static final float[] ILLUMINANT_D60 = { 0.32168f, 0.33767f };
+ /**
+ * Standard CIE 1931 2° illuminant D65, encoded in xyY.
+ * This illuminant has a color temperature of 6504K. This illuminant
+ * is commonly used in RGB color spaces such as sRGB, BT.209, etc.
+ */
+ public static final float[] ILLUMINANT_D65 = { 0.31271f, 0.32902f };
+ /**
+ * Standard CIE 1931 2° illuminant D75, encoded in xyY.
+ * This illuminant has a color temperature of 7504K.
+ */
+ public static final float[] ILLUMINANT_D75 = { 0.29902f, 0.31485f };
+ /**
+ * Standard CIE 1931 2° illuminant E, encoded in xyY.
+ * This illuminant has a color temperature of 5454K.
+ */
+ public static final float[] ILLUMINANT_E = { 0.33333f, 0.33333f };
+
+ /**
+ * The minimum ID value a color space can have.
+ *
+ * @see #getId()
+ */
+ public static final int MIN_ID = -1; // Do not change
+ /**
+ * The maximum ID value a color space can have.
+ *
+ * @see #getId()
+ */
+ public static final int MAX_ID = 63; // Do not change, used to encode in longs
+
+ private static final float[] SRGB_PRIMARIES = { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f };
+ private static final float[] NTSC_1953_PRIMARIES = { 0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f };
+ private static final float[] ILLUMINANT_D50_XYZ = { 0.964212f, 1.0f, 0.825188f };
+
+ // See static initialization block next to #get(Named)
+ private static final ColorSpace[] sNamedColorSpaces = new ColorSpace[Named.values().length];
+
+ @NonNull private final String mName;
+ @NonNull private final Model mModel;
+ @IntRange(from = MIN_ID, to = MAX_ID) private final int mId;
+
+ /**
+ * {@usesMathJax}
+ *
+ * <p>List of common, named color spaces. A corresponding instance of
+ * {@link ColorSpace} can be obtained by calling {@link ColorSpace#get(Named)}:</p>
+ *
+ * <pre class="prettyprint">
+ * ColorSpace cs = ColorSpace.get(ColorSpace.Named.DCI_P3);
+ * </pre>
+ *
+ * <p>The properties of each color space are described below (see {@link #SRGB sRGB}
+ * for instance). When applicable, the color gamut of each color space is compared
+ * to the color gamut of sRGB using a CIE 1931 xy chromaticity diagram. This diagram
+ * shows the location of the color space's primaries and white point.</p>
+ *
+ * @see ColorSpace#get(Named)
+ */
+ public enum Named {
+ // NOTE: Do NOT change the order of the enum
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space sRGB standardized as IEC 61966-2.1:1999.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.640</td><td>0.300</td><td>0.150</td><td>0.3127</td></tr>
+ * <tr><td>y</td><td>0.330</td><td>0.600</td><td>0.060</td><td>0.3290</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">sRGB IEC61966-2.1</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{sRGB} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \lt 0.0031308 \\
+ * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \ge 0.0031308 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{sRGB}}{12.92} & C_{sRGB} \lt 0.04045 \\
+ * \left( \frac{C_{sRGB} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \ge 0.04045 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_srgb.png" />
+ * <figcaption style="text-align: center;">sRGB</figcaption>
+ * </p>
+ */
+ SRGB,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space sRGB standardized as IEC 61966-2.1:1999.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.640</td><td>0.300</td><td>0.150</td><td>0.3127</td></tr>
+ * <tr><td>y</td><td>0.330</td><td>0.600</td><td>0.060</td><td>0.3290</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">sRGB IEC61966-2.1 (Linear)</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(C_{sRGB} = C_{linear}\)</td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(C_{linear} = C_{sRGB}\)</td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_srgb.png" />
+ * <figcaption style="text-align: center;">sRGB</figcaption>
+ * </p>
+ */
+ LINEAR_SRGB,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space scRGB-nl standardized as IEC 61966-2-2:2003.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.640</td><td>0.300</td><td>0.150</td><td>0.3127</td></tr>
+ * <tr><td>y</td><td>0.330</td><td>0.600</td><td>0.060</td><td>0.3290</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">scRGB-nl IEC 61966-2-2:2003</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{scRGB} = \begin{cases} sign(C_{linear}) 12.92 \times \left| C_{linear} \right| &
+ * \left| C_{linear} \right| \lt 0.0031308 \\
+ * sign(C_{linear}) 1.055 \times \left| C_{linear} \right| ^{\frac{1}{2.4}} - 0.055 &
+ * \left| C_{linear} \right| \ge 0.0031308 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{linear} = \begin{cases}sign(C_{scRGB}) \frac{\left| C_{scRGB} \right|}{12.92} &
+ * \left| C_{scRGB} \right| \lt 0.04045 \\
+ * sign(C_{scRGB}) \left( \frac{\left| C_{scRGB} \right| + 0.055}{1.055} \right) ^{2.4} &
+ * \left| C_{scRGB} \right| \ge 0.04045 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([-0.799..2.399[\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_scrgb.png" />
+ * <figcaption style="text-align: center;">Extended sRGB (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ EXTENDED_SRGB,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space scRGB standardized as IEC 61966-2-2:2003.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.640</td><td>0.300</td><td>0.150</td><td>0.3127</td></tr>
+ * <tr><td>y</td><td>0.330</td><td>0.600</td><td>0.060</td><td>0.3290</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">scRGB IEC 61966-2-2:2003</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(C_{scRGB} = C_{linear}\)</td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(C_{linear} = C_{scRGB}\)</td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([-0.5..7.499[\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_scrgb.png" />
+ * <figcaption style="text-align: center;">Extended sRGB (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ LINEAR_EXTENDED_SRGB,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space BT.709 standardized as Rec. ITU-R BT.709-5.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.640</td><td>0.300</td><td>0.150</td><td>0.3127</td></tr>
+ * <tr><td>y</td><td>0.330</td><td>0.600</td><td>0.060</td><td>0.3290</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">Rec. ITU-R BT.709-5</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\
+ * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\
+ * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_bt709.png" />
+ * <figcaption style="text-align: center;">BT.709</figcaption>
+ * </p>
+ */
+ BT709,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space BT.2020 standardized as Rec. ITU-R BT.2020-1.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.708</td><td>0.170</td><td>0.131</td><td>0.3127</td></tr>
+ * <tr><td>y</td><td>0.292</td><td>0.797</td><td>0.046</td><td>0.3290</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">Rec. ITU-R BT.2020-1</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{BT2020} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.0181 \\
+ * 1.0993 \times C_{linear}^{\frac{1}{2.2}} - 0.0993 & C_{linear} \ge 0.0181 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{BT2020}}{4.5} & C_{BT2020} \lt 0.08145 \\
+ * \left( \frac{C_{BT2020} + 0.0993}{1.0993} \right) ^{2.2} & C_{BT2020} \ge 0.08145 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_bt2020.png" />
+ * <figcaption style="text-align: center;">BT.2020 (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ BT2020,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space DCI-P3 standardized as SMPTE RP 431-2-2007.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.680</td><td>0.265</td><td>0.150</td><td>0.314</td></tr>
+ * <tr><td>y</td><td>0.320</td><td>0.690</td><td>0.060</td><td>0.351</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">SMPTE RP 431-2-2007 DCI (P3)</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">N/A</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(C_{P3} = C_{linear}^{\frac{1}{2.6}}\)</td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(C_{linear} = C_{P3}^{2.6}\)</td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_dci_p3.png" />
+ * <figcaption style="text-align: center;">DCI-P3 (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ DCI_P3,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space Display P3 based on SMPTE RP 431-2-2007 and IEC 61966-2.1:1999.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.680</td><td>0.265</td><td>0.150</td><td>0.3127</td></tr>
+ * <tr><td>y</td><td>0.320</td><td>0.690</td><td>0.060</td><td>0.3290</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">Display P3</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{DisplayP3} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \lt 0.0030186 \\
+ * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \ge 0.0030186 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{DisplayP3}}{12.92} & C_{sRGB} \lt 0.039 \\
+ * \left( \frac{C_{DisplayP3} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \ge 0.039 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_display_p3.png" />
+ * <figcaption style="text-align: center;">Display P3 (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ DISPLAY_P3,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space NTSC, 1953 standard.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.67</td><td>0.21</td><td>0.14</td><td>0.310</td></tr>
+ * <tr><td>y</td><td>0.33</td><td>0.71</td><td>0.08</td><td>0.316</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">NTSC (1953)</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">C</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\
+ * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\
+ * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_ntsc_1953.png" />
+ * <figcaption style="text-align: center;">NTSC 1953 (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ NTSC_1953,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space SMPTE C.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.630</td><td>0.310</td><td>0.155</td><td>0.3127</td></tr>
+ * <tr><td>y</td><td>0.340</td><td>0.595</td><td>0.070</td><td>0.3290</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">SMPTE-C RGB</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\
+ * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\
+ * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_smpte_c.png" />
+ * <figcaption style="text-align: center;">SMPTE-C (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ SMPTE_C,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space Adobe RGB (1998).</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.64</td><td>0.21</td><td>0.15</td><td>0.3127</td></tr>
+ * <tr><td>y</td><td>0.33</td><td>0.71</td><td>0.06</td><td>0.3290</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">Adobe RGB (1998)</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(C_{RGB} = C_{linear}^{\frac{1}{2.2}}\)</td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(C_{linear} = C_{RGB}^{2.2}\)</td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_adobe_rgb.png" />
+ * <figcaption style="text-align: center;">Adobe RGB (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ ADOBE_RGB,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space ProPhoto RGB standardized as ROMM RGB ISO 22028-2:2013.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.7347</td><td>0.1596</td><td>0.0366</td><td>0.3457</td></tr>
+ * <tr><td>y</td><td>0.2653</td><td>0.8404</td><td>0.0001</td><td>0.3585</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">ROMM RGB ISO 22028-2:2013</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D50</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{ROMM} = \begin{cases} 16 \times C_{linear} & C_{linear} \lt 0.001953 \\
+ * C_{linear}^{\frac{1}{1.8}} & C_{linear} \ge 0.001953 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{ROMM}}{16} & C_{ROMM} \lt 0.031248 \\
+ * C_{ROMM}^{1.8} & C_{ROMM} \ge 0.031248 \end{cases}
+ * \end{equation}\)
+ * </td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_pro_photo_rgb.png" />
+ * <figcaption style="text-align: center;">ProPhoto RGB (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ PRO_PHOTO_RGB,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space ACES standardized as SMPTE ST 2065-1:2012.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.73470</td><td>0.00000</td><td>0.00010</td><td>0.32168</td></tr>
+ * <tr><td>y</td><td>0.26530</td><td>1.00000</td><td>-0.07700</td><td>0.33767</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">SMPTE ST 2065-1:2012 ACES</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D60</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(C_{ACES} = C_{linear}\)</td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(C_{linear} = C_{ACES}\)</td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([-65504.0, 65504.0]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_aces.png" />
+ * <figcaption style="text-align: center;">ACES (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ ACES,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space ACEScg standardized as Academy S-2014-004.</p>
+ * <table summary="Color space definition">
+ * <tr>
+ * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th>
+ * </tr>
+ * <tr><td>x</td><td>0.713</td><td>0.165</td><td>0.128</td><td>0.32168</td></tr>
+ * <tr><td>y</td><td>0.293</td><td>0.830</td><td>0.044</td><td>0.33767</td></tr>
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">Academy S-2014-004 ACEScg</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D60</td></tr>
+ * <tr>
+ * <td>Opto-electronic transfer function (OETF)</td>
+ * <td colspan="4">\(C_{ACEScg} = C_{linear}\)</td>
+ * </tr>
+ * <tr>
+ * <td>Electro-optical transfer function (EOTF)</td>
+ * <td colspan="4">\(C_{linear} = C_{ACEScg}\)</td>
+ * </tr>
+ * <tr><td>Range</td><td colspan="4">\([-65504.0, 65504.0]\)</td></tr>
+ * </table>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_acescg.png" />
+ * <figcaption style="text-align: center;">ACEScg (orange) vs sRGB (white)</figcaption>
+ * </p>
+ */
+ ACESCG,
+ /**
+ * <p>{@link Model#XYZ XYZ} color space CIE XYZ. This color space assumes standard
+ * illuminant D50 as its white point.</p>
+ * <table summary="Color space definition">
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">Generic XYZ</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D50</td></tr>
+ * <tr><td>Range</td><td colspan="4">\([-2.0, 2.0]\)</td></tr>
+ * </table>
+ */
+ CIE_XYZ,
+ /**
+ * <p>{@link Model#LAB Lab} color space CIE L*a*b*. This color space uses CIE XYZ D50
+ * as a profile conversion space.</p>
+ * <table summary="Color space definition">
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">Generic L*a*b*</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D50</td></tr>
+ * <tr><td>Range</td><td colspan="4">\(L: [0.0, 100.0], a: [-128, 128], b: [-128, 128]\)</td></tr>
+ * </table>
+ */
+ CIE_LAB
+ // Update the initialization block next to #get(Named) when adding new values
+ }
+
+ /**
+ * <p>A render intent determines how a {@link ColorSpace.Connector connector}
+ * maps colors from one color space to another. The choice of mapping is
+ * important when the source color space has a larger color gamut than the
+ * destination color space.</p>
+ *
+ * @see ColorSpace#connect(ColorSpace, ColorSpace, RenderIntent)
+ */
+ public enum RenderIntent {
+ /**
+ * <p>Compresses the source gamut into the destination gamut.
+ * This render intent affects all colors, inside and outside
+ * of destination gamut. The goal of this render intent is
+ * to preserve the visual relationship between colors.</p>
+ *
+ * <p class="note">This render intent is currently not
+ * implemented and behaves like {@link #RELATIVE}.</p>
+ */
+ PERCEPTUAL,
+ /**
+ * Similar to the {@link #ABSOLUTE} render intent, this render
+ * intent matches the closest color in the destination gamut
+ * but makes adjustments for the destination white point.
+ */
+ RELATIVE,
+ /**
+ * <p>Attempts to maintain the relative saturation of colors
+ * from the source gamut to the destination gamut, to keep
+ * highly saturated colors as saturated as possible.</p>
+ *
+ * <p class="note">This render intent is currently not
+ * implemented and behaves like {@link #RELATIVE}.</p>
+ */
+ SATURATION,
+ /**
+ * Colors that are in the destination gamut are left unchanged.
+ * Colors that fall outside of the destination gamut are mapped
+ * to the closest possible color within the gamut of the destination
+ * color space (they are clipped).
+ */
+ ABSOLUTE
+ }
+
+ /**
+ * {@usesMathJax}
+ *
+ * <p>List of adaptation matrices that can be used for chromatic adaptation
+ * using the von Kries transform. These matrices are used to convert values
+ * in the CIE XYZ space to values in the LMS space (Long Medium Short).</p>
+ *
+ * <p>Given an adaptation matrix \(A\), the conversion from XYZ to
+ * LMS is straightforward:</p>
+ *
+ * $$\left[ \begin{array}{c} L\\ M\\ S \end{array} \right] =
+ * A \left[ \begin{array}{c} X\\ Y\\ Z \end{array} \right]$$
+ *
+ * <p>The complete von Kries transform \(T\) uses a diagonal matrix
+ * noted \(D\) to perform the adaptation in LMS space. In addition
+ * to \(A\) and \(D\), the source white point \(W1\) and the destination
+ * white point \(W2\) must be specified:</p>
+ *
+ * $$\begin{align*}
+ * \left[ \begin{array}{c} L_1\\ M_1\\ S_1 \end{array} \right] &=
+ * A \left[ \begin{array}{c} W1_X\\ W1_Y\\ W1_Z \end{array} \right] \\
+ * \left[ \begin{array}{c} L_2\\ M_2\\ S_2 \end{array} \right] &=
+ * A \left[ \begin{array}{c} W2_X\\ W2_Y\\ W2_Z \end{array} \right] \\
+ * D &= \left[ \begin{matrix} \frac{L_2}{L_1} & 0 & 0 \\
+ * 0 & \frac{M_2}{M_1} & 0 \\
+ * 0 & 0 & \frac{S_2}{S_1} \end{matrix} \right] \\
+ * T &= A^{-1}.D.A
+ * \end{align*}$$
+ *
+ * <p>As an example, the resulting matrix \(T\) can then be used to
+ * perform the chromatic adaptation of sRGB XYZ transform from D65
+ * to D50:</p>
+ *
+ * $$sRGB_{D50} = T.sRGB_{D65}$$
+ *
+ * @see ColorSpace.Connector
+ * @see ColorSpace#connect(ColorSpace, ColorSpace)
+ */
+ public enum Adaptation {
+ /**
+ * Bradford chromatic adaptation transform, as defined in the
+ * CIECAM97s color appearance model.
+ */
+ BRADFORD(new float[] {
+ 0.8951f, -0.7502f, 0.0389f,
+ 0.2664f, 1.7135f, -0.0685f,
+ -0.1614f, 0.0367f, 1.0296f
+ }),
+ /**
+ * von Kries chromatic adaptation transform.
+ */
+ VON_KRIES(new float[] {
+ 0.40024f, -0.22630f, 0.00000f,
+ 0.70760f, 1.16532f, 0.00000f,
+ -0.08081f, 0.04570f, 0.91822f
+ }),
+ /**
+ * CIECAT02 chromatic adaption transform, as defined in the
+ * CIECAM02 color appearance model.
+ */
+ CIECAT02(new float[] {
+ 0.7328f, -0.7036f, 0.0030f,
+ 0.4296f, 1.6975f, 0.0136f,
+ -0.1624f, 0.0061f, 0.9834f
+ });
+
+ final float[] mTransform;
+
+ Adaptation(@NonNull @Size(9) float[] transform) {
+ mTransform = transform;
+ }
+ }
+
+ /**
+ * A color model is required by a {@link ColorSpace} to describe the
+ * way colors can be represented as tuples of numbers. A common color
+ * model is the {@link #RGB RGB} color model which defines a color
+ * as represented by a tuple of 3 numbers (red, green and blue).
+ */
+ public enum Model {
+ /**
+ * The RGB model is a color model with 3 components that
+ * refer to the three additive primiaries: red, green
+ * andd blue.
+ */
+ RGB(3),
+ /**
+ * The XYZ model is a color model with 3 components that
+ * are used to model human color vision on a basic sensory
+ * level.
+ */
+ XYZ(3),
+ /**
+ * The Lab model is a color model with 3 components used
+ * to describe a color space that is more perceptually
+ * uniform than XYZ.
+ */
+ LAB(3),
+ /**
+ * The CMYK model is a color model with 4 components that
+ * refer to four inks used in color printing: cyan, magenta,
+ * yellow and black (or key). CMYK is a subtractive color
+ * model.
+ */
+ CMYK(4);
+
+ private final int mComponentCount;
+
+ Model(@IntRange(from = 1, to = 4) int componentCount) {
+ mComponentCount = componentCount;
+ }
+
+ /**
+ * Returns the number of components for this color model.
+ *
+ * @return An integer between 1 and 4
+ */
+ @IntRange(from = 1, to = 4)
+ public int getComponentCount() {
+ return mComponentCount;
+ }
+ }
+
+ private ColorSpace(
+ @NonNull String name,
+ @NonNull Model model,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+
+ if (name == null || name.length() < 1) {
+ throw new IllegalArgumentException("The name of a color space cannot be null and " +
+ "must contain at least 1 character");
+ }
+
+ if (model == null) {
+ throw new IllegalArgumentException("A color space must have a model");
+ }
+
+ if (id < MIN_ID || id > MAX_ID) {
+ throw new IllegalArgumentException("The id must be between " +
+ MIN_ID + " and " + MAX_ID);
+ }
+
+ mName = name;
+ mModel = model;
+ mId = id;
+ }
+
+ /**
+ * <p>Returns the name of this color space. The name is never null
+ * and contains always at least 1 character.</p>
+ *
+ * <p>Color space names are recommended to be unique but are not
+ * guaranteed to be. There is no defined format but the name usually
+ * falls in one of the following categories:</p>
+ * <ul>
+ * <li>Generic names used to identify color spaces in non-RGB
+ * color models. For instance: {@link Named#CIE_LAB Generic L*a*b*}.</li>
+ * <li>Names tied to a particular specification. For instance:
+ * {@link Named#SRGB sRGB IEC61966-2.1} or
+ * {@link Named#ACES SMPTE ST 2065-1:2012 ACES}.</li>
+ * <li>Ad-hoc names, often generated procedurally or by the user
+ * during a calibration workflow. These names often contain the
+ * make and model of the display.</li>
+ * </ul>
+ *
+ * <p>Because the format of color space names is not defined, it is
+ * not recommended to programmatically identify a color space by its
+ * name alone. Names can be used as a first approximation.</p>
+ *
+ * <p>It is however perfectly acceptable to display color space names to
+ * users in a UI, or in debuggers and logs. When displaying a color space
+ * name to the user, it is recommended to add extra information to avoid
+ * ambiguities: color model, a representation of the color space's gamut,
+ * white point, etc.</p>
+ *
+ * @return A non-null String of length >= 1
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the ID of this color space. Positive IDs match the color
+ * spaces enumerated in {@link Named}. A negative ID indicates a
+ * color space created by calling one of the public constructors.
+ *
+ * @return An integer between {@link #MIN_ID} and {@link #MAX_ID}
+ */
+ @IntRange(from = MIN_ID, to = MAX_ID)
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Return the color model of this color space.
+ *
+ * @return A non-null {@link Model}
+ *
+ * @see Model
+ * @see #getComponentCount()
+ */
+ @NonNull
+ public Model getModel() {
+ return mModel;
+ }
+
+ /**
+ * Returns the number of components that form a color value according
+ * to this color space's color model.
+ *
+ * @return An integer between 1 and 4
+ *
+ * @see Model
+ * @see #getModel()
+ */
+ @IntRange(from = 1, to = 4)
+ public int getComponentCount() {
+ return mModel.getComponentCount();
+ }
+
+ /**
+ * Returns whether this color space is a wide-gamut color space.
+ * An RGB color space is wide-gamut if its gamut entirely contains
+ * the {@link Named#SRGB sRGB} gamut and if the area of its gamut is
+ * 90% of greater than the area of the {@link Named#NTSC_1953 NTSC}
+ * gamut.
+ *
+ * @return True if this color space is a wide-gamut color space,
+ * false otherwise
+ */
+ public abstract boolean isWideGamut();
+
+ /**
+ * <p>Indicates whether this color space is the sRGB color space or
+ * equivalent to the sRGB color space.</p>
+ * <p>A color space is considered sRGB if it meets all the following
+ * conditions:</p>
+ * <ul>
+ * <li>Its color model is {@link Model#RGB}.</li>
+ * <li>
+ * Its primaries are within 1e-3 of the true
+ * {@link Named#SRGB sRGB} primaries.
+ * </li>
+ * <li>
+ * Its white point is withing 1e-3 of the CIE standard
+ * illuminant {@link #ILLUMINANT_D65 D65}.
+ * </li>
+ * <li>Its opto-electronic transfer function is not linear.</li>
+ * <li>Its electro-optical transfer function is not linear.</li>
+ * <li>Its range is \([0..1]\).</li>
+ * </ul>
+ * <p>This method always returns true for {@link Named#SRGB}.</p>
+ *
+ * @return True if this color space is the sRGB color space (or a
+ * close approximation), false otherwise
+ */
+ public boolean isSrgb() {
+ return false;
+ }
+
+ /**
+ * Returns the minimum valid value for the specified component of this
+ * color space's color model.
+ *
+ * @param component The index of the component
+ * @return A floating point value less than {@link #getMaxValue(int)}
+ *
+ * @see #getMaxValue(int)
+ * @see Model#getComponentCount()
+ */
+ public abstract float getMinValue(@IntRange(from = 0, to = 3) int component);
+
+ /**
+ * Returns the maximum valid value for the specified component of this
+ * color space's color model.
+ *
+ * @param component The index of the component
+ * @return A floating point value greater than {@link #getMinValue(int)}
+ *
+ * @see #getMinValue(int)
+ * @see Model#getComponentCount()
+ */
+ public abstract float getMaxValue(@IntRange(from = 0, to = 3) int component);
+
+ /**
+ * <p>Converts a color value from this color space's model to
+ * tristimulus CIE XYZ values. If the color model of this color
+ * space is not {@link Model#RGB RGB}, it is assumed that the
+ * target CIE XYZ space uses a {@link #ILLUMINANT_D50 D50}
+ * standard illuminant.</p>
+ *
+ * <p>This method is a convenience for color spaces with a model
+ * of 3 components ({@link Model#RGB RGB} or {@link Model#LAB}
+ * for instance). With color spaces using fewer or more components,
+ * use {@link #toXyz(float[])} instead</p>.
+ *
+ * @param r The first component of the value to convert from (typically R in RGB)
+ * @param g The second component of the value to convert from (typically G in RGB)
+ * @param b The third component of the value to convert from (typically B in RGB)
+ * @return A new array of 3 floats, containing tristimulus XYZ values
+ *
+ * @see #toXyz(float[])
+ * @see #fromXyz(float, float, float)
+ */
+ @NonNull
+ @Size(3)
+ public float[] toXyz(float r, float g, float b) {
+ return toXyz(new float[] { r, g, b });
+ }
+
+ /**
+ * <p>Converts a color value from this color space's model to
+ * tristimulus CIE XYZ values. If the color model of this color
+ * space is not {@link Model#RGB RGB}, it is assumed that the
+ * target CIE XYZ space uses a {@link #ILLUMINANT_D50 D50}
+ * standard illuminant.</p>
+ *
+ * <p class="note">The specified array's length must be at least
+ * equal to to the number of color components as returned by
+ * {@link Model#getComponentCount()}.</p>
+ *
+ * @param v An array of color components containing the color space's
+ * color value to convert to XYZ, and large enough to hold
+ * the resulting tristimulus XYZ values
+ * @return The array passed in parameter
+ *
+ * @see #toXyz(float, float, float)
+ * @see #fromXyz(float[])
+ */
+ @NonNull
+ @Size(min = 3)
+ public abstract float[] toXyz(@NonNull @Size(min = 3) float[] v);
+
+ /**
+ * <p>Converts tristimulus values from the CIE XYZ space to this
+ * color space's color model.</p>
+ *
+ * @param x The X component of the color value
+ * @param y The Y component of the color value
+ * @param z The Z component of the color value
+ * @return A new array whose size is equal to the number of color
+ * components as returned by {@link Model#getComponentCount()}
+ *
+ * @see #fromXyz(float[])
+ * @see #toXyz(float, float, float)
+ */
+ @NonNull
+ @Size(min = 3)
+ public float[] fromXyz(float x, float y, float z) {
+ float[] xyz = new float[mModel.getComponentCount()];
+ xyz[0] = x;
+ xyz[1] = y;
+ xyz[2] = z;
+ return fromXyz(xyz);
+ }
+
+ /**
+ * <p>Converts tristimulus values from the CIE XYZ space to this color
+ * space's color model. The resulting value is passed back in the specified
+ * array.</p>
+ *
+ * <p class="note">The specified array's length must be at least equal to
+ * to the number of color components as returned by
+ * {@link Model#getComponentCount()}, and its first 3 values must
+ * be the XYZ components to convert from.</p>
+ *
+ * @param v An array of color components containing the XYZ values
+ * to convert from, and large enough to hold the number
+ * of components of this color space's model
+ * @return The array passed in parameter
+ *
+ * @see #fromXyz(float, float, float)
+ * @see #toXyz(float[])
+ */
+ @NonNull
+ @Size(min = 3)
+ public abstract float[] fromXyz(@NonNull @Size(min = 3) float[] v);
+
+ /**
+ * <p>Returns a string representation of the object. This method returns
+ * a string equal to the value of:</p>
+ *
+ * <pre class="prettyprint">
+ * getName() + "(id=" + getId() + ", model=" + getModel() + ")"
+ * </pre>
+ *
+ * <p>For instance, the string representation of the {@link Named#SRGB sRGB}
+ * color space is equal to the following value:</p>
+ *
+ * <pre>
+ * sRGB IEC61966-2.1 (id=0, model=RGB)
+ * </pre>
+ *
+ * @return A string representation of the object
+ */
+ @Override
+ @NonNull
+ public String toString() {
+ return mName + " (id=" + mId + ", model=" + mModel + ")";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ColorSpace that = (ColorSpace) o;
+
+ if (mId != that.mId) return false;
+ //noinspection SimplifiableIfStatement
+ if (!mName.equals(that.mName)) return false;
+ return mModel == that.mModel;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mName.hashCode();
+ result = 31 * result + mModel.hashCode();
+ result = 31 * result + mId;
+ return result;
+ }
+
+ /**
+ * <p>Connects two color spaces to allow conversion from the source color
+ * space to the destination color space. If the source and destination
+ * color spaces do not have the same profile connection space (CIE XYZ
+ * with the same white point), they are chromatically adapted to use the
+ * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.</p>
+ *
+ * <p>If the source and destination are the same, an optimized connector
+ * is returned to avoid unnecessary computations and loss of precision.</p>
+ *
+ * <p>Colors are mapped from the source color space to the destination color
+ * space using the {@link RenderIntent#PERCEPTUAL perceptual} render intent.</p>
+ *
+ * @param source The color space to convert colors from
+ * @param destination The color space to convert colors to
+ * @return A non-null connector between the two specified color spaces
+ *
+ * @see #connect(ColorSpace)
+ * @see #connect(ColorSpace, RenderIntent)
+ * @see #connect(ColorSpace, ColorSpace, RenderIntent)
+ */
+ @NonNull
+ public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination) {
+ return connect(source, destination, RenderIntent.PERCEPTUAL);
+ }
+
+ /**
+ * <p>Connects two color spaces to allow conversion from the source color
+ * space to the destination color space. If the source and destination
+ * color spaces do not have the same profile connection space (CIE XYZ
+ * with the same white point), they are chromatically adapted to use the
+ * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.</p>
+ *
+ * <p>If the source and destination are the same, an optimized connector
+ * is returned to avoid unnecessary computations and loss of precision.</p>
+ *
+ * @param source The color space to convert colors from
+ * @param destination The color space to convert colors to
+ * @param intent The render intent to map colors from the source to the destination
+ * @return A non-null connector between the two specified color spaces
+ *
+ * @see #connect(ColorSpace)
+ * @see #connect(ColorSpace, RenderIntent)
+ * @see #connect(ColorSpace, ColorSpace)
+ */
+ @NonNull
+ @SuppressWarnings("ConstantConditions")
+ public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull RenderIntent intent) {
+ if (source.equals(destination)) return Connector.identity(source);
+
+ if (source.getModel() == Model.RGB && destination.getModel() == Model.RGB) {
+ return new Connector.Rgb((Rgb) source, (Rgb) destination, intent);
+ }
+
+ return new Connector(source, destination, intent);
+ }
+
+ /**
+ * <p>Connects the specified color spaces to sRGB.
+ * If the source color space does not use CIE XYZ D65 as its profile
+ * connection space, the two spaces are chromatically adapted to use the
+ * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.</p>
+ *
+ * <p>If the source is the sRGB color space, an optimized connector
+ * is returned to avoid unnecessary computations and loss of precision.</p>
+ *
+ * <p>Colors are mapped from the source color space to the destination color
+ * space using the {@link RenderIntent#PERCEPTUAL perceptual} render intent.</p>
+ *
+ * @param source The color space to convert colors from
+ * @return A non-null connector between the specified color space and sRGB
+ *
+ * @see #connect(ColorSpace, RenderIntent)
+ * @see #connect(ColorSpace, ColorSpace)
+ * @see #connect(ColorSpace, ColorSpace, RenderIntent)
+ */
+ @NonNull
+ public static Connector connect(@NonNull ColorSpace source) {
+ return connect(source, RenderIntent.PERCEPTUAL);
+ }
+
+ /**
+ * <p>Connects the specified color spaces to sRGB.
+ * If the source color space does not use CIE XYZ D65 as its profile
+ * connection space, the two spaces are chromatically adapted to use the
+ * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.</p>
+ *
+ * <p>If the source is the sRGB color space, an optimized connector
+ * is returned to avoid unnecessary computations and loss of precision.</p>
+ *
+ * @param source The color space to convert colors from
+ * @param intent The render intent to map colors from the source to the destination
+ * @return A non-null connector between the specified color space and sRGB
+ *
+ * @see #connect(ColorSpace)
+ * @see #connect(ColorSpace, ColorSpace)
+ * @see #connect(ColorSpace, ColorSpace, RenderIntent)
+ */
+ @NonNull
+ public static Connector connect(@NonNull ColorSpace source, @NonNull RenderIntent intent) {
+ if (source.isSrgb()) return Connector.identity(source);
+
+ if (source.getModel() == Model.RGB) {
+ return new Connector.Rgb((Rgb) source, (Rgb) get(Named.SRGB), intent);
+ }
+
+ return new Connector(source, get(Named.SRGB), intent);
+ }
+
+ /**
+ * <p>Performs the chromatic adaptation of a color space from its native
+ * white point to the specified white point.</p>
+ *
+ * <p>The chromatic adaptation is performed using the
+ * {@link Adaptation#BRADFORD} matrix.</p>
+ *
+ * <p class="note">The color space returned by this method always has
+ * an ID of {@link #MIN_ID}.</p>
+ *
+ * @param colorSpace The color space to chromatically adapt
+ * @param whitePoint The new white point
+ * @return A {@link ColorSpace} instance with the same name, primaries,
+ * transfer functions and range as the specified color space
+ *
+ * @see Adaptation
+ * @see #adapt(ColorSpace, float[], Adaptation)
+ */
+ @NonNull
+ public static ColorSpace adapt(@NonNull ColorSpace colorSpace,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint) {
+ return adapt(colorSpace, whitePoint, Adaptation.BRADFORD);
+ }
+
+ /**
+ * <p>Performs the chromatic adaptation of a color space from its native
+ * white point to the specified white point. If the specified color space
+ * does not have an {@link Model#RGB RGB} color model, or if the color
+ * space already has the target white point, the color space is returned
+ * unmodified.</p>
+ *
+ * <p>The chromatic adaptation is performed using the von Kries method
+ * described in the documentation of {@link Adaptation}.</p>
+ *
+ * <p class="note">The color space returned by this method always has
+ * an ID of {@link #MIN_ID}.</p>
+ *
+ * @param colorSpace The color space to chromatically adapt
+ * @param whitePoint The new white point
+ * @param adaptation The adaptation matrix
+ * @return A new color space if the specified color space has an RGB
+ * model and a white point different from the specified white
+ * point; the specified color space otherwise
+ *
+ * @see Adaptation
+ * @see #adapt(ColorSpace, float[])
+ */
+ @NonNull
+ public static ColorSpace adapt(@NonNull ColorSpace colorSpace,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull Adaptation adaptation) {
+ if (colorSpace.getModel() == Model.RGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace;
+ if (compare(rgb.mWhitePoint, whitePoint)) return colorSpace;
+
+ float[] xyz = whitePoint.length == 3 ?
+ Arrays.copyOf(whitePoint, 3) : xyYToXyz(whitePoint);
+ float[] adaptationTransform = chromaticAdaptation(adaptation.mTransform,
+ xyYToXyz(rgb.getWhitePoint()), xyz);
+ float[] transform = mul3x3(adaptationTransform, rgb.mTransform);
+
+ return new ColorSpace.Rgb(rgb, transform, whitePoint);
+ }
+ return colorSpace;
+ }
+
+ /**
+ * <p>Returns an instance of {@link ColorSpace} whose ID matches the
+ * specified ID.</p>
+ *
+ * <p>This method always returns the same instance for a given ID.</p>
+ *
+ * <p>This method is thread-safe.</p>
+ *
+ * @param index An integer ID between {@link #MIN_ID} and {@link #MAX_ID}
+ * @return A non-null {@link ColorSpace} instance
+ * @throws IllegalArgumentException If the ID does not match the ID of one of the
+ * {@link Named named color spaces}
+ */
+ @NonNull
+ static ColorSpace get(@IntRange(from = MIN_ID, to = MAX_ID) int index) {
+ if (index < 0 || index > Named.values().length) {
+ throw new IllegalArgumentException("Invalid ID, must be in the range [0.." +
+ Named.values().length + "]");
+ }
+ return sNamedColorSpaces[index];
+ }
+
+ /**
+ * <p>Returns an instance of {@link ColorSpace} identified by the specified
+ * name. The list of names provided in the {@link Named} enum gives access
+ * to a variety of common RGB color spaces.</p>
+ *
+ * <p>This method always returns the same instance for a given name.</p>
+ *
+ * <p>This method is thread-safe.</p>
+ *
+ * @param name The name of the color space to get an instance of
+ * @return A non-null {@link ColorSpace} instance
+ */
+ @NonNull
+ public static ColorSpace get(@NonNull Named name) {
+ return sNamedColorSpaces[name.ordinal()];
+ }
+
+ /**
+ * <p>Returns a {@link Named} instance of {@link ColorSpace} that matches
+ * the specified RGB to CIE XYZ transform and transfer functions. If no
+ * instance can be found, this method returns null.</p>
+ *
+ * <p>The color transform matrix is assumed to target the CIE XYZ space
+ * a {@link #ILLUMINANT_D50 D50} standard illuminant.</p>
+ *
+ * @param toXYZD50 3x3 column-major transform matrix from RGB to the profile
+ * connection space CIE XYZ as an array of 9 floats, cannot be null
+ * @param function Parameters for the transfer functions
+ * @return A non-null {@link ColorSpace} if a match is found, null otherwise
+ */
+ @Nullable
+ public static ColorSpace match(
+ @NonNull @Size(9) float[] toXYZD50,
+ @NonNull Rgb.TransferParameters function) {
+
+ for (ColorSpace colorSpace : sNamedColorSpaces) {
+ if (colorSpace.getModel() == Model.RGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) adapt(colorSpace, ILLUMINANT_D50_XYZ);
+ if (compare(toXYZD50, rgb.mTransform) &&
+ compare(function, rgb.mTransferParameters)) {
+ return colorSpace;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * <p>Creates a new {@link Renderer} that can be used to visualize and
+ * debug color spaces. See the documentation of {@link Renderer} for
+ * more information.</p>
+ *
+ * @return A new non-null {@link Renderer} instance
+ *
+ * @see Renderer
+ *
+ * @hide
+ */
+ @NonNull
+ public static Renderer createRenderer() {
+ return new Renderer();
+ }
+
+ static {
+ sNamedColorSpaces[Named.SRGB.ordinal()] = new ColorSpace.Rgb(
+ "sRGB IEC61966-2.1",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4),
+ Named.SRGB.ordinal()
+ );
+ sNamedColorSpaces[Named.LINEAR_SRGB.ordinal()] = new ColorSpace.Rgb(
+ "sRGB IEC61966-2.1 (Linear)",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ 1.0,
+ 0.0f, 1.0f,
+ Named.LINEAR_SRGB.ordinal()
+ );
+ sNamedColorSpaces[Named.EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb(
+ "scRGB-nl IEC 61966-2-2:2003",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ x -> absRcpResponse(x, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4),
+ x -> absResponse(x, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4),
+ -0.799f, 2.399f,
+ Named.EXTENDED_SRGB.ordinal()
+ );
+ sNamedColorSpaces[Named.LINEAR_EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb(
+ "scRGB IEC 61966-2-2:2003",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ 1.0,
+ -0.5f, 7.499f,
+ Named.LINEAR_EXTENDED_SRGB.ordinal()
+ );
+ sNamedColorSpaces[Named.BT709.ordinal()] = new ColorSpace.Rgb(
+ "Rec. ITU-R BT.709-5",
+ new float[] { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f },
+ ILLUMINANT_D65,
+ new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45),
+ Named.BT709.ordinal()
+ );
+ sNamedColorSpaces[Named.BT2020.ordinal()] = new ColorSpace.Rgb(
+ "Rec. ITU-R BT.2020-1",
+ new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f },
+ ILLUMINANT_D65,
+ new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45),
+ Named.BT2020.ordinal()
+ );
+ sNamedColorSpaces[Named.DCI_P3.ordinal()] = new ColorSpace.Rgb(
+ "SMPTE RP 431-2-2007 DCI (P3)",
+ new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f },
+ new float[] { 0.314f, 0.351f },
+ 2.6,
+ 0.0f, 1.0f,
+ Named.DCI_P3.ordinal()
+ );
+ sNamedColorSpaces[Named.DISPLAY_P3.ordinal()] = new ColorSpace.Rgb(
+ "Display P3",
+ new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f },
+ ILLUMINANT_D65,
+ new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.039, 2.4),
+ Named.DISPLAY_P3.ordinal()
+ );
+ sNamedColorSpaces[Named.NTSC_1953.ordinal()] = new ColorSpace.Rgb(
+ "NTSC (1953)",
+ NTSC_1953_PRIMARIES,
+ ILLUMINANT_C,
+ new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45),
+ Named.NTSC_1953.ordinal()
+ );
+ sNamedColorSpaces[Named.SMPTE_C.ordinal()] = new ColorSpace.Rgb(
+ "SMPTE-C RGB",
+ new float[] { 0.630f, 0.340f, 0.310f, 0.595f, 0.155f, 0.070f },
+ ILLUMINANT_D65,
+ new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45),
+ Named.SMPTE_C.ordinal()
+ );
+ sNamedColorSpaces[Named.ADOBE_RGB.ordinal()] = new ColorSpace.Rgb(
+ "Adobe RGB (1998)",
+ new float[] { 0.64f, 0.33f, 0.21f, 0.71f, 0.15f, 0.06f },
+ ILLUMINANT_D65,
+ 2.2,
+ 0.0f, 1.0f,
+ Named.ADOBE_RGB.ordinal()
+ );
+ sNamedColorSpaces[Named.PRO_PHOTO_RGB.ordinal()] = new ColorSpace.Rgb(
+ "ROMM RGB ISO 22028-2:2013",
+ new float[] { 0.7347f, 0.2653f, 0.1596f, 0.8404f, 0.0366f, 0.0001f },
+ ILLUMINANT_D50,
+ new Rgb.TransferParameters(1.0, 0.0, 1 / 16.0, 0.031248, 1.8),
+ Named.PRO_PHOTO_RGB.ordinal()
+ );
+ sNamedColorSpaces[Named.ACES.ordinal()] = new ColorSpace.Rgb(
+ "SMPTE ST 2065-1:2012 ACES",
+ new float[] { 0.73470f, 0.26530f, 0.0f, 1.0f, 0.00010f, -0.0770f },
+ ILLUMINANT_D60,
+ 1.0,
+ -65504.0f, 65504.0f,
+ Named.ACES.ordinal()
+ );
+ sNamedColorSpaces[Named.ACESCG.ordinal()] = new ColorSpace.Rgb(
+ "Academy S-2014-004 ACEScg",
+ new float[] { 0.713f, 0.293f, 0.165f, 0.830f, 0.128f, 0.044f },
+ ILLUMINANT_D60,
+ 1.0,
+ -65504.0f, 65504.0f,
+ Named.ACESCG.ordinal()
+ );
+ sNamedColorSpaces[Named.CIE_XYZ.ordinal()] = new Xyz(
+ "Generic XYZ",
+ Named.CIE_XYZ.ordinal()
+ );
+ sNamedColorSpaces[Named.CIE_LAB.ordinal()] = new ColorSpace.Lab(
+ "Generic L*a*b*",
+ Named.CIE_LAB.ordinal()
+ );
+ }
+
+ // Reciprocal piecewise gamma response
+ private static double rcpResponse(double x, double a, double b, double c, double d, double g) {
+ return x >= d * c ? (Math.pow(x, 1.0 / g) - b) / a : x / c;
+ }
+
+ // Piecewise gamma response
+ private static double response(double x, double a, double b, double c, double d, double g) {
+ return x >= d ? Math.pow(a * x + b, g) : c * x;
+ }
+
+ // Reciprocal piecewise gamma response
+ private static double rcpResponse(double x, double a, double b, double c, double d,
+ double e, double f, double g) {
+ return x >= d * c ? (Math.pow(x - e, 1.0 / g) - b) / a : (x - f) / c;
+ }
+
+ // Piecewise gamma response
+ private static double response(double x, double a, double b, double c, double d,
+ double e, double f, double g) {
+ return x >= d ? Math.pow(a * x + b, g) + e : c * x + f;
+ }
+
+ // Reciprocal piecewise gamma response, encoded as sign(x).f(abs(x)) for color
+ // spaces that allow negative values
+ @SuppressWarnings("SameParameterValue")
+ private static double absRcpResponse(double x, double a, double b, double c, double d, double g) {
+ return Math.copySign(rcpResponse(x < 0.0 ? -x : x, a, b, c, d, g), x);
+ }
+
+ // Piecewise gamma response, encoded as sign(x).f(abs(x)) for color spaces that
+ // allow negative values
+ @SuppressWarnings("SameParameterValue")
+ private static double absResponse(double x, double a, double b, double c, double d, double g) {
+ return Math.copySign(response(x < 0.0 ? -x : x, a, b, c, d, g), x);
+ }
+
+ /**
+ * Compares two sets of parametric transfer functions parameters with a precision of 1e-3.
+ *
+ * @param a The first set of parameters to compare
+ * @param b The second set of parameters to compare
+ * @return True if the two sets are equal, false otherwise
+ */
+ private static boolean compare(
+ @Nullable Rgb.TransferParameters a,
+ @Nullable Rgb.TransferParameters b) {
+ //noinspection SimplifiableIfStatement
+ if (a == null && b == null) return true;
+ return a != null && b != null &&
+ Math.abs(a.a - b.a) < 1e-3 &&
+ Math.abs(a.b - b.b) < 1e-3 &&
+ Math.abs(a.c - b.c) < 1e-3 &&
+ Math.abs(a.d - b.d) < 2e-3 && // Special case for variations in sRGB OETF/EOTF
+ Math.abs(a.e - b.e) < 1e-3 &&
+ Math.abs(a.f - b.f) < 1e-3 &&
+ Math.abs(a.g - b.g) < 1e-3;
+ }
+
+ /**
+ * Compares two arrays of float with a precision of 1e-3.
+ *
+ * @param a The first array to compare
+ * @param b The second array to compare
+ * @return True if the two arrays are equal, false otherwise
+ */
+ private static boolean compare(@NonNull float[] a, @NonNull float[] b) {
+ if (a == b) return true;
+ for (int i = 0; i < a.length; i++) {
+ if (Float.compare(a[i], b[i]) != 0 && Math.abs(a[i] - b[i]) > 1e-3f) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Inverts a 3x3 matrix. This method assumes the matrix is invertible.
+ *
+ * @param m A 3x3 matrix as a non-null array of 9 floats
+ * @return A new array of 9 floats containing the inverse of the input matrix
+ */
+ @NonNull
+ @Size(9)
+ private static float[] inverse3x3(@NonNull @Size(9) float[] m) {
+ float a = m[0];
+ float b = m[3];
+ float c = m[6];
+ float d = m[1];
+ float e = m[4];
+ float f = m[7];
+ float g = m[2];
+ float h = m[5];
+ float i = m[8];
+
+ float A = e * i - f * h;
+ float B = f * g - d * i;
+ float C = d * h - e * g;
+
+ float det = a * A + b * B + c * C;
+
+ float inverted[] = new float[m.length];
+ inverted[0] = A / det;
+ inverted[1] = B / det;
+ inverted[2] = C / det;
+ inverted[3] = (c * h - b * i) / det;
+ inverted[4] = (a * i - c * g) / det;
+ inverted[5] = (b * g - a * h) / det;
+ inverted[6] = (b * f - c * e) / det;
+ inverted[7] = (c * d - a * f) / det;
+ inverted[8] = (a * e - b * d) / det;
+ return inverted;
+ }
+
+ /**
+ * Multiplies two 3x3 matrices, represented as non-null arrays of 9 floats.
+ *
+ * @param lhs 3x3 matrix, as a non-null array of 9 floats
+ * @param rhs 3x3 matrix, as a non-null array of 9 floats
+ * @return A new array of 9 floats containing the result of the multiplication
+ * of rhs by lhs
+ */
+ @NonNull
+ @Size(9)
+ private static float[] mul3x3(@NonNull @Size(9) float[] lhs, @NonNull @Size(9) float[] rhs) {
+ float[] r = new float[9];
+ r[0] = lhs[0] * rhs[0] + lhs[3] * rhs[1] + lhs[6] * rhs[2];
+ r[1] = lhs[1] * rhs[0] + lhs[4] * rhs[1] + lhs[7] * rhs[2];
+ r[2] = lhs[2] * rhs[0] + lhs[5] * rhs[1] + lhs[8] * rhs[2];
+ r[3] = lhs[0] * rhs[3] + lhs[3] * rhs[4] + lhs[6] * rhs[5];
+ r[4] = lhs[1] * rhs[3] + lhs[4] * rhs[4] + lhs[7] * rhs[5];
+ r[5] = lhs[2] * rhs[3] + lhs[5] * rhs[4] + lhs[8] * rhs[5];
+ r[6] = lhs[0] * rhs[6] + lhs[3] * rhs[7] + lhs[6] * rhs[8];
+ r[7] = lhs[1] * rhs[6] + lhs[4] * rhs[7] + lhs[7] * rhs[8];
+ r[8] = lhs[2] * rhs[6] + lhs[5] * rhs[7] + lhs[8] * rhs[8];
+ return r;
+ }
+
+ /**
+ * Multiplies a vector of 3 components by a 3x3 matrix and stores the
+ * result in the input vector.
+ *
+ * @param lhs 3x3 matrix, as a non-null array of 9 floats
+ * @param rhs Vector of 3 components, as a non-null array of 3 floats
+ * @return The array of 3 passed as the rhs parameter
+ */
+ @NonNull
+ @Size(min = 3)
+ private static float[] mul3x3Float3(
+ @NonNull @Size(9) float[] lhs, @NonNull @Size(min = 3) float[] rhs) {
+ float r0 = rhs[0];
+ float r1 = rhs[1];
+ float r2 = rhs[2];
+ rhs[0] = lhs[0] * r0 + lhs[3] * r1 + lhs[6] * r2;
+ rhs[1] = lhs[1] * r0 + lhs[4] * r1 + lhs[7] * r2;
+ rhs[2] = lhs[2] * r0 + lhs[5] * r1 + lhs[8] * r2;
+ return rhs;
+ }
+
+ /**
+ * Multiplies a diagonal 3x3 matrix lhs, represented as an array of 3 floats,
+ * by a 3x3 matrix represented as an array of 9 floats.
+ *
+ * @param lhs Diagonal 3x3 matrix, as a non-null array of 3 floats
+ * @param rhs 3x3 matrix, as a non-null array of 9 floats
+ * @return A new array of 9 floats containing the result of the multiplication
+ * of rhs by lhs
+ */
+ @NonNull
+ @Size(9)
+ private static float[] mul3x3Diag(
+ @NonNull @Size(3) float[] lhs, @NonNull @Size(9) float[] rhs) {
+ return new float[] {
+ lhs[0] * rhs[0], lhs[1] * rhs[1], lhs[2] * rhs[2],
+ lhs[0] * rhs[3], lhs[1] * rhs[4], lhs[2] * rhs[5],
+ lhs[0] * rhs[6], lhs[1] * rhs[7], lhs[2] * rhs[8]
+ };
+ }
+
+ /**
+ * Converts a value from CIE xyY to CIE XYZ. Y is assumed to be 1 so the
+ * input xyY array only contains the x and y components.
+ *
+ * @param xyY The xyY value to convert to XYZ, cannot be null, length must be 2
+ * @return A new float array of length 3 containing XYZ values
+ */
+ @NonNull
+ @Size(3)
+ private static float[] xyYToXyz(@NonNull @Size(2) float[] xyY) {
+ return new float[] { xyY[0] / xyY[1], 1.0f, (1 - xyY[0] - xyY[1]) / xyY[1] };
+ }
+
+ /**
+ * Converts values from CIE xyY to CIE L*u*v*. Y is assumed to be 1 so the
+ * input xyY array only contains the x and y components. After this method
+ * returns, the xyY array contains the converted u and v components.
+ *
+ * @param xyY The xyY value to convert to XYZ, cannot be null,
+ * length must be a multiple of 2
+ */
+ private static void xyYToUv(@NonNull @Size(multiple = 2) float[] xyY) {
+ for (int i = 0; i < xyY.length; i += 2) {
+ float x = xyY[i];
+ float y = xyY[i + 1];
+
+ float d = -2.0f * x + 12.0f * y + 3;
+ float u = (4.0f * x) / d;
+ float v = (9.0f * y) / d;
+
+ xyY[i] = u;
+ xyY[i + 1] = v;
+ }
+ }
+
+ /**
+ * <p>Computes the chromatic adaptation transform from the specified
+ * source white point to the specified destination white point.</p>
+ *
+ * <p>The transform is computed using the von Kries method, described
+ * in more details in the documentation of {@link Adaptation}. The
+ * {@link Adaptation} enum provides different matrices that can be
+ * used to perform the adaptation.</p>
+ *
+ * @param matrix The adaptation matrix
+ * @param srcWhitePoint The white point to adapt from, *will be modified*
+ * @param dstWhitePoint The white point to adapt to, *will be modified*
+ * @return A 3x3 matrix as a non-null array of 9 floats
+ */
+ @NonNull
+ @Size(9)
+ private static float[] chromaticAdaptation(@NonNull @Size(9) float[] matrix,
+ @NonNull @Size(3) float[] srcWhitePoint, @NonNull @Size(3) float[] dstWhitePoint) {
+ float[] srcLMS = mul3x3Float3(matrix, srcWhitePoint);
+ float[] dstLMS = mul3x3Float3(matrix, dstWhitePoint);
+ // LMS is a diagonal matrix stored as a float[3]
+ float[] LMS = { dstLMS[0] / srcLMS[0], dstLMS[1] / srcLMS[1], dstLMS[2] / srcLMS[2] };
+ return mul3x3(inverse3x3(matrix), mul3x3Diag(LMS, matrix));
+ }
+
+ /**
+ * Implementation of the CIE XYZ color space. Assumes the white point is D50.
+ */
+ @AnyThread
+ private static final class Xyz extends ColorSpace {
+ private Xyz(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ super(name, Model.XYZ, id);
+ }
+
+ @Override
+ public boolean isWideGamut() {
+ return true;
+ }
+
+ @Override
+ public float getMinValue(@IntRange(from = 0, to = 3) int component) {
+ return -2.0f;
+ }
+
+ @Override
+ public float getMaxValue(@IntRange(from = 0, to = 3) int component) {
+ return 2.0f;
+ }
+
+ @Override
+ public float[] toXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = clamp(v[0]);
+ v[1] = clamp(v[1]);
+ v[2] = clamp(v[2]);
+ return v;
+ }
+
+ @Override
+ public float[] fromXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = clamp(v[0]);
+ v[1] = clamp(v[1]);
+ v[2] = clamp(v[2]);
+ return v;
+ }
+
+ private static float clamp(float x) {
+ return x < -2.0f ? -2.0f : x > 2.0f ? 2.0f : x;
+ }
+ }
+
+ /**
+ * Implementation of the CIE L*a*b* color space. Its PCS is CIE XYZ
+ * with a white point of D50.
+ */
+ @AnyThread
+ private static final class Lab extends ColorSpace {
+ private static final float A = 216.0f / 24389.0f;
+ private static final float B = 841.0f / 108.0f;
+ private static final float C = 4.0f / 29.0f;
+ private static final float D = 6.0f / 29.0f;
+
+ private Lab(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ super(name, Model.LAB, id);
+ }
+
+ @Override
+ public boolean isWideGamut() {
+ return true;
+ }
+
+ @Override
+ public float getMinValue(@IntRange(from = 0, to = 3) int component) {
+ return component == 0 ? 0.0f : -128.0f;
+ }
+
+ @Override
+ public float getMaxValue(@IntRange(from = 0, to = 3) int component) {
+ return component == 0 ? 100.0f : 128.0f;
+ }
+
+ @Override
+ public float[] toXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = clamp(v[0], 0.0f, 100.0f);
+ v[1] = clamp(v[1], -128.0f, 128.0f);
+ v[2] = clamp(v[2], -128.0f, 128.0f);
+
+ float fy = (v[0] + 16.0f) / 116.0f;
+ float fx = fy + (v[1] * 0.002f);
+ float fz = fy - (v[2] * 0.005f);
+ float X = fx > D ? fx * fx * fx : (1.0f / B) * (fx - C);
+ float Y = fy > D ? fy * fy * fy : (1.0f / B) * (fy - C);
+ float Z = fz > D ? fz * fz * fz : (1.0f / B) * (fz - C);
+
+ v[0] = X * ILLUMINANT_D50_XYZ[0];
+ v[1] = Y * ILLUMINANT_D50_XYZ[1];
+ v[2] = Z * ILLUMINANT_D50_XYZ[2];
+
+ return v;
+ }
+
+ @Override
+ public float[] fromXyz(@NonNull @Size(min = 3) float[] v) {
+ float X = v[0] / ILLUMINANT_D50_XYZ[0];
+ float Y = v[1] / ILLUMINANT_D50_XYZ[1];
+ float Z = v[2] / ILLUMINANT_D50_XYZ[2];
+
+ float fx = X > A ? (float) Math.pow(X, 1.0 / 3.0) : B * X + C;
+ float fy = Y > A ? (float) Math.pow(Y, 1.0 / 3.0) : B * Y + C;
+ float fz = Z > A ? (float) Math.pow(Z, 1.0 / 3.0) : B * Z + C;
+
+ float L = 116.0f * fy - 16.0f;
+ float a = 500.0f * (fx - fy);
+ float b = 200.0f * (fy - fz);
+
+ v[0] = clamp(L, 0.0f, 100.0f);
+ v[1] = clamp(a, -128.0f, 128.0f);
+ v[2] = clamp(b, -128.0f, 128.0f);
+
+ return v;
+ }
+
+ private static float clamp(float x, float min, float max) {
+ return x < min ? min : x > max ? max : x;
+ }
+ }
+
+ /**
+ * {@usesMathJax}
+ *
+ * <p>An RGB color space is an additive color space using the
+ * {@link Model#RGB RGB} color model (a color is therefore represented
+ * by a tuple of 3 numbers).</p>
+ *
+ * <p>A specific RGB color space is defined by the following properties:</p>
+ * <ul>
+ * <li>Three chromaticities of the red, green and blue primaries, which
+ * define the gamut of the color space.</li>
+ * <li>A white point chromaticity that defines the stimulus to which
+ * color space values are normalized (also just called "white").</li>
+ * <li>An opto-electronic transfer function, also called opto-electronic
+ * conversion function or often, and approximately, gamma function.</li>
+ * <li>An electro-optical transfer function, also called electo-optical
+ * conversion function or often, and approximately, gamma function.</li>
+ * <li>A range of valid RGB values (most commonly \([0..1]\)).</li>
+ * </ul>
+ *
+ * <p>The most commonly used RGB color space is {@link Named#SRGB sRGB}.</p>
+ *
+ * <h3>Primaries and white point chromaticities</h3>
+ * <p>In this implementation, the chromaticity of the primaries and the white
+ * point of an RGB color space is defined in the CIE xyY color space. This
+ * color space separates the chromaticity of a color, the x and y components,
+ * and its luminance, the Y component. Since the primaries and the white
+ * point have full brightness, the Y component is assumed to be 1 and only
+ * the x and y components are needed to encode them.</p>
+ * <p>For convenience, this implementation also allows to define the
+ * primaries and white point in the CIE XYZ space. The tristimulus XYZ values
+ * are internally converted to xyY.</p>
+ *
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_srgb.png" />
+ * <figcaption style="text-align: center;">sRGB primaries and white point</figcaption>
+ * </p>
+ *
+ * <h3>Transfer functions</h3>
+ * <p>A transfer function is a color component conversion function, defined as
+ * a single variable, monotonic mathematical function. It is applied to each
+ * individual component of a color. They are used to perform the mapping
+ * between linear tristimulus values and non-linear electronic signal value.</p>
+ * <p>The <em>opto-electronic transfer function</em> (OETF or OECF) encodes
+ * tristimulus values in a scene to a non-linear electronic signal value.
+ * An OETF is often expressed as a power function with an exponent between
+ * 0.38 and 0.55 (the reciprocal of 1.8 to 2.6).</p>
+ * <p>The <em>electro-optical transfer function</em> (EOTF or EOCF) decodes
+ * a non-linear electronic signal value to a tristimulus value at the display.
+ * An EOTF is often expressed as a power function with an exponent between
+ * 1.8 and 2.6.</p>
+ * <p>Transfer functions are used as a compression scheme. For instance,
+ * linear sRGB values would normally require 11 to 12 bits of precision to
+ * store all values that can be perceived by the human eye. When encoding
+ * sRGB values using the appropriate OETF (see {@link Named#SRGB sRGB} for
+ * an exact mathematical description of that OETF), the values can be
+ * compressed to only 8 bits precision.</p>
+ * <p>When manipulating RGB values, particularly sRGB values, it is safe
+ * to assume that these values have been encoded with the appropriate
+ * OETF (unless noted otherwise). Encoded values are often said to be in
+ * "gamma space". They are therefore defined in a non-linear space. This
+ * in turns means that any linear operation applied to these values is
+ * going to yield mathematically incorrect results (any linear interpolation
+ * such as gradient generation for instance, most image processing functions
+ * such as blurs, etc.).</p>
+ * <p>To properly process encoded RGB values you must first apply the
+ * EOTF to decode the value into linear space. After processing, the RGB
+ * value must be encoded back to non-linear ("gamma") space. Here is a
+ * formal description of the process, where \(f\) is the processing
+ * function to apply:</p>
+ *
+ * $$RGB_{out} = OETF(f(EOTF(RGB_{in})))$$
+ *
+ * <p>If the transfer functions of the color space can be expressed as an
+ * ICC parametric curve as defined in ICC.1:2004-10, the numeric parameters
+ * can be retrieved by calling {@link #getTransferParameters()}. This can
+ * be useful to match color spaces for instance.</p>
+ *
+ * <p class="note">Some RGB color spaces, such as {@link Named#ACES} and
+ * {@link Named#LINEAR_EXTENDED_SRGB scRGB}, are said to be linear because
+ * their transfer functions are the identity function: \(f(x) = x\).
+ * If the source and/or destination are known to be linear, it is not
+ * necessary to invoke the transfer functions.</p>
+ *
+ * <h3>Range</h3>
+ * <p>Most RGB color spaces allow RGB values in the range \([0..1]\). There
+ * are however a few RGB color spaces that allow much larger ranges. For
+ * instance, {@link Named#EXTENDED_SRGB scRGB} is used to manipulate the
+ * range \([-0.5..7.5]\) while {@link Named#ACES ACES} can be used throughout
+ * the range \([-65504, 65504]\).</p>
+ *
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_scrgb.png" />
+ * <figcaption style="text-align: center;">Extended sRGB and its large range</figcaption>
+ * </p>
+ *
+ * <h3>Converting between RGB color spaces</h3>
+ * <p>Conversion between two color spaces is achieved by using an intermediate
+ * color space called the profile connection space (PCS). The PCS used by
+ * this implementation is CIE XYZ. The conversion operation is defined
+ * as such:</p>
+ *
+ * $$RGB_{out} = OETF(T_{dst}^{-1} \cdot T_{src} \cdot EOTF(RGB_{in}))$$
+ *
+ * <p>Where \(T_{src}\) is the {@link #getTransform() RGB to XYZ transform}
+ * of the source color space and \(T_{dst}^{-1}\) the {@link #getInverseTransform()
+ * XYZ to RGB transform} of the destination color space.</p>
+ * <p>Many RGB color spaces commonly used with electronic devices use the
+ * standard illuminant {@link #ILLUMINANT_D65 D65}. Care must be take however
+ * when converting between two RGB color spaces if their white points do not
+ * match. This can be achieved by either calling
+ * {@link #adapt(ColorSpace, float[])} to adapt one or both color spaces to
+ * a single common white point. This can be achieved automatically by calling
+ * {@link ColorSpace#connect(ColorSpace, ColorSpace)}, which also handles
+ * non-RGB color spaces.</p>
+ * <p>To learn more about the white point adaptation process, refer to the
+ * documentation of {@link Adaptation}.</p>
+ */
+ @AnyThread
+ public static class Rgb extends ColorSpace {
+ /**
+ * {@usesMathJax}
+ *
+ * <p>Defines the parameters for the ICC parametric curve type 4, as
+ * defined in ICC.1:2004-10, section 10.15.</p>
+ *
+ * <p>The EOTF is of the form:</p>
+ *
+ * \(\begin{equation}
+ * Y = \begin{cases}c X + f & X \lt d \\
+ * \left( a X + b \right) ^{g} + e & X \ge d \end{cases}
+ * \end{equation}\)
+ *
+ * <p>The corresponding OETF is simply the inverse function.</p>
+ *
+ * <p>The parameters defined by this class form a valid transfer
+ * function only if all the following conditions are met:</p>
+ * <ul>
+ * <li>No parameter is a {@link Double#isNaN(double) Not-a-Number}</li>
+ * <li>\(d\) is in the range \([0..1]\)</li>
+ * <li>The function is not constant</li>
+ * <li>The function is positive and increasing</li>
+ * </ul>
+ */
+ public static class TransferParameters {
+ /** Variable \(a\) in the equation of the EOTF described above. */
+ public final double a;
+ /** Variable \(b\) in the equation of the EOTF described above. */
+ public final double b;
+ /** Variable \(c\) in the equation of the EOTF described above. */
+ public final double c;
+ /** Variable \(d\) in the equation of the EOTF described above. */
+ public final double d;
+ /** Variable \(e\) in the equation of the EOTF described above. */
+ public final double e;
+ /** Variable \(f\) in the equation of the EOTF described above. */
+ public final double f;
+ /** Variable \(g\) in the equation of the EOTF described above. */
+ public final double g;
+
+ /**
+ * <p>Defines the parameters for the ICC parametric curve type 3, as
+ * defined in ICC.1:2004-10, section 10.15.</p>
+ *
+ * <p>The EOTF is of the form:</p>
+ *
+ * \(\begin{equation}
+ * Y = \begin{cases}c X & X \lt d \\
+ * \left( a X + b \right) ^{g} & X \ge d \end{cases}
+ * \end{equation}\)
+ *
+ * <p>This constructor is equivalent to setting \(e\) and \(f\) to 0.</p>
+ *
+ * @param a The value of \(a\) in the equation of the EOTF described above
+ * @param b The value of \(b\) in the equation of the EOTF described above
+ * @param c The value of \(c\) in the equation of the EOTF described above
+ * @param d The value of \(d\) in the equation of the EOTF described above
+ * @param g The value of \(g\) in the equation of the EOTF described above
+ *
+ * @throws IllegalArgumentException If the parameters form an invalid transfer function
+ */
+ public TransferParameters(double a, double b, double c, double d, double g) {
+ this(a, b, c, d, 0.0, 0.0, g);
+ }
+
+ /**
+ * <p>Defines the parameters for the ICC parametric curve type 4, as
+ * defined in ICC.1:2004-10, section 10.15.</p>
+ *
+ * @param a The value of \(a\) in the equation of the EOTF described above
+ * @param b The value of \(b\) in the equation of the EOTF described above
+ * @param c The value of \(c\) in the equation of the EOTF described above
+ * @param d The value of \(d\) in the equation of the EOTF described above
+ * @param e The value of \(e\) in the equation of the EOTF described above
+ * @param f The value of \(f\) in the equation of the EOTF described above
+ * @param g The value of \(g\) in the equation of the EOTF described above
+ *
+ * @throws IllegalArgumentException If the parameters form an invalid transfer function
+ */
+ public TransferParameters(double a, double b, double c, double d, double e,
+ double f, double g) {
+
+ if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c) ||
+ Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f) ||
+ Double.isNaN(g)) {
+ throw new IllegalArgumentException("Parameters cannot be NaN");
+ }
+
+ // Next representable float after 1.0
+ // We use doubles here but the representation inside our native code is often floats
+ if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) {
+ throw new IllegalArgumentException("Parameter d must be in the range [0..1], " +
+ "was " + d);
+ }
+
+ if (d == 0.0 && (a == 0.0 || g == 0.0)) {
+ throw new IllegalArgumentException(
+ "Parameter a or g is zero, the transfer function is constant");
+ }
+
+ if (d >= 1.0 && c == 0.0) {
+ throw new IllegalArgumentException(
+ "Parameter c is zero, the transfer function is constant");
+ }
+
+ if ((a == 0.0 || g == 0.0) && c == 0.0) {
+ throw new IllegalArgumentException("Parameter a or g is zero," +
+ " and c is zero, the transfer function is constant");
+ }
+
+ if (c < 0.0) {
+ throw new IllegalArgumentException("The transfer function must be increasing");
+ }
+
+ if (a < 0.0 || g < 0.0) {
+ throw new IllegalArgumentException("The transfer function must be " +
+ "positive or increasing");
+ }
+
+ this.a = a;
+ this.b = b;
+ this.c = c;
+ this.d = d;
+ this.e = e;
+ this.f = f;
+ this.g = g;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TransferParameters that = (TransferParameters) o;
+
+ if (Double.compare(that.a, a) != 0) return false;
+ if (Double.compare(that.b, b) != 0) return false;
+ if (Double.compare(that.c, c) != 0) return false;
+ if (Double.compare(that.d, d) != 0) return false;
+ if (Double.compare(that.e, e) != 0) return false;
+ if (Double.compare(that.f, f) != 0) return false;
+ return Double.compare(that.g, g) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ long temp;
+ temp = Double.doubleToLongBits(a);
+ result = (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(b);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(c);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(d);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(e);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(f);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(g);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+ }
+
+ @NonNull private final float[] mWhitePoint;
+ @NonNull private final float[] mPrimaries;
+ @NonNull private final float[] mTransform;
+ @NonNull private final float[] mInverseTransform;
+
+ @NonNull private final DoubleUnaryOperator mOetf;
+ @NonNull private final DoubleUnaryOperator mEotf;
+ @NonNull private final DoubleUnaryOperator mClampedOetf;
+ @NonNull private final DoubleUnaryOperator mClampedEotf;
+
+ private final float mMin;
+ private final float mMax;
+
+ private final boolean mIsWideGamut;
+ private final boolean mIsSrgb;
+
+ @Nullable private TransferParameters mTransferParameters;
+
+ /**
+ * <p>Creates a new RGB color space using a 3x3 column-major transform matrix.
+ * The transform matrix must convert from the RGB space to the profile connection
+ * space CIE XYZ.</p>
+ *
+ * <p class="note">The range of the color space is imposed to be \([0..1]\).</p>
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param toXYZ 3x3 column-major transform matrix from RGB to the profile
+ * connection space CIE XYZ as an array of 9 floats, cannot be null
+ * @param oetf Opto-electronic transfer function, cannot be null
+ * @param eotf Electro-optical transfer function, cannot be null
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ * <ul>
+ * <li>The name is null or has a length of 0.</li>
+ * <li>The OETF is null or the EOTF is null.</li>
+ * <li>The minimum valid value is >= the maximum valid value.</li>
+ * </ul>
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(9) float[] toXYZ,
+ @NonNull DoubleUnaryOperator oetf,
+ @NonNull DoubleUnaryOperator eotf) {
+ this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ),
+ oetf, eotf, 0.0f, 1.0f, MIN_ID);
+ }
+
+ /**
+ * <p>Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.</p>
+ *
+ * <p>The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:</p>
+ *
+ * <table summary="Parameters length">
+ * <tr><th>Space</th><th>Primaries length</th><th>White point length</th></tr>
+ * <tr><td>xyY</td><td>6</td><td>2</td></tr>
+ * <tr><td>XYZ</td><td>9</td><td>3</td></tr>
+ * </table>
+ *
+ * <p>When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.</p>
+ *
+ * <p class="note">The ID, areturned by {@link #getId()}, of an object created by
+ * this constructor is always {@link #MIN_ID}.</p>
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param oetf Opto-electronic transfer function, cannot be null
+ * @param eotf Electro-optical transfer function, cannot be null
+ * @param min The minimum valid value in this color space's RGB range
+ * @param max The maximum valid value in this color space's RGB range
+ *
+ * @throws IllegalArgumentException <p>If any of the following conditions is met:</p>
+ * <ul>
+ * <li>The name is null or has a length of 0.</li>
+ * <li>The primaries array is null or has a length that is neither 6 or 9.</li>
+ * <li>The white point array is null or has a length that is neither 2 or 3.</li>
+ * <li>The OETF is null or the EOTF is null.</li>
+ * <li>The minimum valid value is >= the maximum valid value.</li>
+ * </ul>
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull DoubleUnaryOperator oetf,
+ @NonNull DoubleUnaryOperator eotf,
+ float min,
+ float max) {
+ this(name, primaries, whitePoint, oetf, eotf, min, max, MIN_ID);
+ }
+
+ /**
+ * <p>Creates a new RGB color space using a 3x3 column-major transform matrix.
+ * The transform matrix must convert from the RGB space to the profile connection
+ * space CIE XYZ.</p>
+ *
+ * <p class="note">The range of the color space is imposed to be \([0..1]\).</p>
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param toXYZ 3x3 column-major transform matrix from RGB to the profile
+ * connection space CIE XYZ as an array of 9 floats, cannot be null
+ * @param function Parameters for the transfer functions
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ * <ul>
+ * <li>The name is null or has a length of 0.</li>
+ * <li>Gamma is negative.</li>
+ * </ul>
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(9) float[] toXYZ,
+ @NonNull TransferParameters function) {
+ this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), function, MIN_ID);
+ }
+
+ /**
+ * <p>Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.</p>
+ *
+ * <p>The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:</p>
+ *
+ * <table summary="Parameters length">
+ * <tr><th>Space</th><th>Primaries length</th><th>White point length</th></tr>
+ * <tr><td>xyY</td><td>6</td><td>2</td></tr>
+ * <tr><td>XYZ</td><td>9</td><td>3</td></tr>
+ * </table>
+ *
+ * <p>When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.</p>
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param function Parameters for the transfer functions
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ * <ul>
+ * <li>The name is null or has a length of 0.</li>
+ * <li>The primaries array is null or has a length that is neither 6 or 9.</li>
+ * <li>The white point array is null or has a length that is neither 2 or 3.</li>
+ * <li>The transfer parameters are invalid.</li>
+ * </ul>
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull TransferParameters function) {
+ this(name, primaries, whitePoint, function, MIN_ID);
+ }
+
+ /**
+ * <p>Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.</p>
+ *
+ * <p>The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:</p>
+ *
+ * <table summary="Parameters length">
+ * <tr><th>Space</th><th>Primaries length</th><th>White point length</th></tr>
+ * <tr><td>xyY</td><td>6</td><td>2</td></tr>
+ * <tr><td>XYZ</td><td>9</td><td>3</td></tr>
+ * </table>
+ *
+ * <p>When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.</p>
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param function Parameters for the transfer functions
+ * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID}
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ * <ul>
+ * <li>The name is null or has a length of 0.</li>
+ * <li>The primaries array is null or has a length that is neither 6 or 9.</li>
+ * <li>The white point array is null or has a length that is neither 2 or 3.</li>
+ * <li>The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.</li>
+ * <li>The transfer parameters are invalid.</li>
+ * </ul>
+ *
+ * @see #get(Named)
+ */
+ private Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull TransferParameters function,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ this(name, primaries, whitePoint,
+ function.e == 0.0 && function.f == 0.0 ?
+ x -> rcpResponse(x, function.a, function.b,
+ function.c, function.d, function.g) :
+ x -> rcpResponse(x, function.a, function.b, function.c,
+ function.d, function.e, function.f, function.g),
+ function.e == 0.0 && function.f == 0.0 ?
+ x -> response(x, function.a, function.b,
+ function.c, function.d, function.g) :
+ x -> response(x, function.a, function.b, function.c,
+ function.d, function.e, function.f, function.g),
+ 0.0f, 1.0f, id);
+ mTransferParameters = function;
+ }
+
+ /**
+ * <p>Creates a new RGB color space using a 3x3 column-major transform matrix.
+ * The transform matrix must convert from the RGB space to the profile connection
+ * space CIE XYZ.</p>
+ *
+ * <p class="note">The range of the color space is imposed to be \([0..1]\).</p>
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param toXYZ 3x3 column-major transform matrix from RGB to the profile
+ * connection space CIE XYZ as an array of 9 floats, cannot be null
+ * @param gamma Gamma to use as the transfer function
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ * <ul>
+ * <li>The name is null or has a length of 0.</li>
+ * <li>Gamma is negative.</li>
+ * </ul>
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(9) float[] toXYZ,
+ double gamma) {
+ this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), gamma, 0.0f, 1.0f, MIN_ID);
+ }
+
+ /**
+ * <p>Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.</p>
+ *
+ * <p>The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:</p>
+ *
+ * <table summary="Parameters length">
+ * <tr><th>Space</th><th>Primaries length</th><th>White point length</th></tr>
+ * <tr><td>xyY</td><td>6</td><td>2</td></tr>
+ * <tr><td>XYZ</td><td>9</td><td>3</td></tr>
+ * </table>
+ *
+ * <p>When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.</p>
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param gamma Gamma to use as the transfer function
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ * <ul>
+ * <li>The name is null or has a length of 0.</li>
+ * <li>The primaries array is null or has a length that is neither 6 or 9.</li>
+ * <li>The white point array is null or has a length that is neither 2 or 3.</li>
+ * <li>Gamma is negative.</li>
+ * </ul>
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ double gamma) {
+ this(name, primaries, whitePoint, gamma, 0.0f, 1.0f, MIN_ID);
+ }
+
+ /**
+ * <p>Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.</p>
+ *
+ * <p>The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:</p>
+ *
+ * <table summary="Parameters length">
+ * <tr><th>Space</th><th>Primaries length</th><th>White point length</th></tr>
+ * <tr><td>xyY</td><td>6</td><td>2</td></tr>
+ * <tr><td>XYZ</td><td>9</td><td>3</td></tr>
+ * </table>
+ *
+ * <p>When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.</p>
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param gamma Gamma to use as the transfer function
+ * @param min The minimum valid value in this color space's RGB range
+ * @param max The maximum valid value in this color space's RGB range
+ * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID}
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ * <ul>
+ * <li>The name is null or has a length of 0.</li>
+ * <li>The primaries array is null or has a length that is neither 6 or 9.</li>
+ * <li>The white point array is null or has a length that is neither 2 or 3.</li>
+ * <li>The minimum valid value is >= the maximum valid value.</li>
+ * <li>The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.</li>
+ * <li>Gamma is negative.</li>
+ * </ul>
+ *
+ * @see #get(Named)
+ */
+ private Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ double gamma,
+ float min,
+ float max,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ this(name, primaries, whitePoint,
+ gamma == 1.0 ? DoubleUnaryOperator.identity() :
+ x -> Math.pow(x < 0.0 ? 0.0 : x, 1 / gamma),
+ gamma == 1.0 ? DoubleUnaryOperator.identity() :
+ x -> Math.pow(x < 0.0 ? 0.0 : x, gamma),
+ min, max, id);
+ mTransferParameters = gamma == 1.0 ?
+ new TransferParameters(0.0, 0.0, 1.0, 1.0 + Math.ulp(1.0f), gamma) :
+ new TransferParameters(1.0, 0.0, 0.0, 0.0, gamma);
+ }
+
+ /**
+ * <p>Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.</p>
+ *
+ * <p>The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:</p>
+ *
+ * <table summary="Parameters length">
+ * <tr><th>Space</th><th>Primaries length</th><th>White point length</th></tr>
+ * <tr><td>xyY</td><td>6</td><td>2</td></tr>
+ * <tr><td>XYZ</td><td>9</td><td>3</td></tr>
+ * </table>
+ *
+ * <p>When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.</p>
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param oetf Opto-electronic transfer function, cannot be null
+ * @param eotf Electro-optical transfer function, cannot be null
+ * @param min The minimum valid value in this color space's RGB range
+ * @param max The maximum valid value in this color space's RGB range
+ * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID}
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ * <ul>
+ * <li>The name is null or has a length of 0.</li>
+ * <li>The primaries array is null or has a length that is neither 6 or 9.</li>
+ * <li>The white point array is null or has a length that is neither 2 or 3.</li>
+ * <li>The OETF is null or the EOTF is null.</li>
+ * <li>The minimum valid value is >= the maximum valid value.</li>
+ * <li>The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.</li>
+ * </ul>
+ *
+ * @see #get(Named)
+ */
+ private Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull DoubleUnaryOperator oetf,
+ @NonNull DoubleUnaryOperator eotf,
+ float min,
+ float max,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+
+ super(name, Model.RGB, id);
+
+ if (primaries == null || (primaries.length != 6 && primaries.length != 9)) {
+ throw new IllegalArgumentException("The color space's primaries must be " +
+ "defined as an array of 6 floats in xyY or 9 floats in XYZ");
+ }
+
+ if (whitePoint == null || (whitePoint.length != 2 && whitePoint.length != 3)) {
+ throw new IllegalArgumentException("The color space's white point must be " +
+ "defined as an array of 2 floats in xyY or 3 float in XYZ");
+ }
+
+ if (oetf == null || eotf == null) {
+ throw new IllegalArgumentException("The transfer functions of a color space " +
+ "cannot be null");
+ }
+
+ if (min >= max) {
+ throw new IllegalArgumentException("Invalid range: min=" + min + ", max=" + max +
+ "; min must be strictly < max");
+ }
+
+ mWhitePoint = xyWhitePoint(whitePoint);
+ mPrimaries = xyPrimaries(primaries);
+
+ mTransform = computeXYZMatrix(mPrimaries, mWhitePoint);
+ mInverseTransform = inverse3x3(mTransform);
+
+ mOetf = oetf;
+ mEotf = eotf;
+
+ mMin = min;
+ mMax = max;
+
+ DoubleUnaryOperator clamp = this::clamp;
+ mClampedOetf = oetf.andThen(clamp);
+ mClampedEotf = clamp.andThen(eotf);
+
+ // A color space is wide-gamut if its area is >90% of NTSC 1953 and
+ // if it entirely contains the Color space definition in xyY
+ mIsWideGamut = isWideGamut(mPrimaries, min, max);
+ mIsSrgb = isSrgb(mPrimaries, mWhitePoint, oetf, eotf, min, max, id);
+ }
+
+ /**
+ * Creates a copy of the specified color space with a new transform.
+ *
+ * @param colorSpace The color space to create a copy of
+ */
+ private Rgb(Rgb colorSpace,
+ @NonNull @Size(9) float[] transform,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint) {
+ super(colorSpace.getName(), Model.RGB, -1);
+
+ mWhitePoint = xyWhitePoint(whitePoint);
+ mPrimaries = colorSpace.mPrimaries;
+
+ mTransform = transform;
+ mInverseTransform = inverse3x3(transform);
+
+ mMin = colorSpace.mMin;
+ mMax = colorSpace.mMax;
+
+ mOetf = colorSpace.mOetf;
+ mEotf = colorSpace.mEotf;
+
+ mClampedOetf = colorSpace.mClampedOetf;
+ mClampedEotf = colorSpace.mClampedEotf;
+
+ mIsWideGamut = colorSpace.mIsWideGamut;
+ mIsSrgb = colorSpace.mIsSrgb;
+
+ mTransferParameters = colorSpace.mTransferParameters;
+ }
+
+ /**
+ * Copies the non-adapted CIE xyY white point of this color space in
+ * specified array. The Y component is assumed to be 1 and is therefore
+ * not copied into the destination. The x and y components are written
+ * in the array at positions 0 and 1 respectively.
+ *
+ * @param whitePoint The destination array, cannot be null, its length
+ * must be >= 2
+ *
+ * @return The destination array passed as a parameter
+ *
+ * @see #getWhitePoint(float[])
+ */
+ @NonNull
+ @Size(min = 2)
+ public float[] getWhitePoint(@NonNull @Size(min = 2) float[] whitePoint) {
+ whitePoint[0] = mWhitePoint[0];
+ whitePoint[1] = mWhitePoint[1];
+ return whitePoint;
+ }
+
+ /**
+ * Returns the non-adapted CIE xyY white point of this color space as
+ * a new array of 2 floats. The Y component is assumed to be 1 and is
+ * therefore not copied into the destination. The x and y components
+ * are written in the array at positions 0 and 1 respectively.
+ *
+ * @return A new non-null array of 2 floats
+ *
+ * @see #getWhitePoint()
+ */
+ @NonNull
+ @Size(2)
+ public float[] getWhitePoint() {
+ return Arrays.copyOf(mWhitePoint, mWhitePoint.length);
+ }
+
+ /**
+ * Copies the primaries of this color space in specified array. The Y
+ * component is assumed to be 1 and is therefore not copied into the
+ * destination. The x and y components of the first primary are written
+ * in the array at positions 0 and 1 respectively.
+ *
+ * @param primaries The destination array, cannot be null, its length
+ * must be >= 6
+ *
+ * @return The destination array passed as a parameter
+ *
+ * @see #getPrimaries(float[])
+ */
+ @NonNull
+ @Size(min = 6)
+ public float[] getPrimaries(@NonNull @Size(min = 6) float[] primaries) {
+ System.arraycopy(mPrimaries, 0, primaries, 0, mPrimaries.length);
+ return primaries;
+ }
+
+ /**
+ * Returns the primaries of this color space as a new array of 6 floats.
+ * The Y component is assumed to be 1 and is therefore not copied into
+ * the destination. The x and y components of the first primary are
+ * written in the array at positions 0 and 1 respectively.
+ *
+ * @return A new non-null array of 2 floats
+ *
+ * @see #getWhitePoint()
+ */
+ @NonNull
+ @Size(6)
+ public float[] getPrimaries() {
+ return Arrays.copyOf(mPrimaries, mPrimaries.length);
+ }
+
+ /**
+ * <p>Copies the transform of this color space in specified array. The
+ * transform is used to convert from RGB to XYZ (with the same white
+ * point as this color space). To connect color spaces, you must first
+ * {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them to the
+ * same white point.</p>
+ * <p>It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)}
+ * to convert between color spaces.</p>
+ *
+ * @param transform The destination array, cannot be null, its length
+ * must be >= 9
+ *
+ * @return The destination array passed as a parameter
+ *
+ * @see #getInverseTransform()
+ */
+ @NonNull
+ @Size(min = 9)
+ public float[] getTransform(@NonNull @Size(min = 9) float[] transform) {
+ System.arraycopy(mTransform, 0, transform, 0, mTransform.length);
+ return transform;
+ }
+
+ /**
+ * <p>Returns the transform of this color space as a new array. The
+ * transform is used to convert from RGB to XYZ (with the same white
+ * point as this color space). To connect color spaces, you must first
+ * {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them to the
+ * same white point.</p>
+ * <p>It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)}
+ * to convert between color spaces.</p>
+ *
+ * @return A new array of 9 floats
+ *
+ * @see #getInverseTransform(float[])
+ */
+ @NonNull
+ @Size(9)
+ public float[] getTransform() {
+ return Arrays.copyOf(mTransform, mTransform.length);
+ }
+
+ /**
+ * <p>Copies the inverse transform of this color space in specified array.
+ * The inverse transform is used to convert from XYZ to RGB (with the
+ * same white point as this color space). To connect color spaces, you
+ * must first {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them
+ * to the same white point.</p>
+ * <p>It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)}
+ * to convert between color spaces.</p>
+ *
+ * @param inverseTransform The destination array, cannot be null, its length
+ * must be >= 9
+ *
+ * @return The destination array passed as a parameter
+ *
+ * @see #getTransform()
+ */
+ @NonNull
+ @Size(min = 9)
+ public float[] getInverseTransform(@NonNull @Size(min = 9) float[] inverseTransform) {
+ System.arraycopy(mInverseTransform, 0, inverseTransform, 0, mInverseTransform.length);
+ return inverseTransform;
+ }
+
+ /**
+ * <p>Returns the inverse transform of this color space as a new array.
+ * The inverse transform is used to convert from XYZ to RGB (with the
+ * same white point as this color space). To connect color spaces, you
+ * must first {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them
+ * to the same white point.</p>
+ * <p>It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)}
+ * to convert between color spaces.</p>
+ *
+ * @return A new array of 9 floats
+ *
+ * @see #getTransform(float[])
+ */
+ @NonNull
+ @Size(9)
+ public float[] getInverseTransform() {
+ return Arrays.copyOf(mInverseTransform, mInverseTransform.length);
+ }
+
+ /**
+ * <p>Returns the opto-electronic transfer function (OETF) of this color space.
+ * The inverse function is the electro-optical transfer function (EOTF) returned
+ * by {@link #getEotf()}. These functions are defined to satisfy the following
+ * equality for \(x \in [0..1]\):</p>
+ *
+ * $$OETF(EOTF(x)) = EOTF(OETF(x)) = x$$
+ *
+ * <p>For RGB colors, this function can be used to convert from linear space
+ * to "gamma space" (gamma encoded). The terms gamma space and gamma encoded
+ * are frequently used because many OETFs can be closely approximated using
+ * a simple power function of the form \(x^{\frac{1}{\gamma}}\) (the
+ * approximation of the {@link Named#SRGB sRGB} OETF uses \(\gamma=2.2\)
+ * for instance).</p>
+ *
+ * @return A transfer function that converts from linear space to "gamma space"
+ *
+ * @see #getEotf()
+ * @see #getTransferParameters()
+ */
+ @NonNull
+ public DoubleUnaryOperator getOetf() {
+ return mClampedOetf;
+ }
+
+ /**
+ * <p>Returns the electro-optical transfer function (EOTF) of this color space.
+ * The inverse function is the opto-electronic transfer function (OETF)
+ * returned by {@link #getOetf()}. These functions are defined to satisfy the
+ * following equality for \(x \in [0..1]\):</p>
+ *
+ * $$OETF(EOTF(x)) = EOTF(OETF(x)) = x$$
+ *
+ * <p>For RGB colors, this function can be used to convert from "gamma space"
+ * (gamma encoded) to linear space. The terms gamma space and gamma encoded
+ * are frequently used because many EOTFs can be closely approximated using
+ * a simple power function of the form \(x^\gamma\) (the approximation of the
+ * {@link Named#SRGB sRGB} EOTF uses \(\gamma=2.2\) for instance).</p>
+ *
+ * @return A transfer function that converts from "gamma space" to linear space
+ *
+ * @see #getOetf()
+ * @see #getTransferParameters()
+ */
+ @NonNull
+ public DoubleUnaryOperator getEotf() {
+ return mClampedEotf;
+ }
+
+ /**
+ * <p>Returns the parameters used by the {@link #getEotf() electro-optical}
+ * and {@link #getOetf() opto-electronic} transfer functions. If the transfer
+ * functions do not match the ICC parametric curves defined in ICC.1:2004-10
+ * (section 10.15), this method returns null.</p>
+ *
+ * <p>See {@link TransferParameters} for a full description of the transfer
+ * functions.</p>
+ *
+ * @return An instance of {@link TransferParameters} or null if this color
+ * space's transfer functions do not match the equation defined in
+ * {@link TransferParameters}
+ */
+ @Nullable
+ public TransferParameters getTransferParameters() {
+ return mTransferParameters;
+ }
+
+ @Override
+ public boolean isSrgb() {
+ return mIsSrgb;
+ }
+
+ @Override
+ public boolean isWideGamut() {
+ return mIsWideGamut;
+ }
+
+ @Override
+ public float getMinValue(int component) {
+ return mMin;
+ }
+
+ @Override
+ public float getMaxValue(int component) {
+ return mMax;
+ }
+
+ /**
+ * <p>Decodes an RGB value to linear space. This is achieved by
+ * applying this color space's electro-optical transfer function
+ * to the supplied values.</p>
+ *
+ * <p>Refer to the documentation of {@link ColorSpace.Rgb} for
+ * more information about transfer functions and their use for
+ * encoding and decoding RGB values.</p>
+ *
+ * @param r The red component to decode to linear space
+ * @param g The green component to decode to linear space
+ * @param b The blue component to decode to linear space
+ * @return A new array of 3 floats containing linear RGB values
+ *
+ * @see #toLinear(float[])
+ * @see #fromLinear(float, float, float)
+ */
+ @NonNull
+ @Size(3)
+ public float[] toLinear(float r, float g, float b) {
+ return toLinear(new float[] { r, g, b });
+ }
+
+ /**
+ * <p>Decodes an RGB value to linear space. This is achieved by
+ * applying this color space's electro-optical transfer function
+ * to the first 3 values of the supplied array. The result is
+ * stored back in the input array.</p>
+ *
+ * <p>Refer to the documentation of {@link ColorSpace.Rgb} for
+ * more information about transfer functions and their use for
+ * encoding and decoding RGB values.</p>
+ *
+ * @param v A non-null array of non-linear RGB values, its length
+ * must be at least 3
+ * @return The specified array
+ *
+ * @see #toLinear(float, float, float)
+ * @see #fromLinear(float[])
+ */
+ @NonNull
+ @Size(min = 3)
+ public float[] toLinear(@NonNull @Size(min = 3) float[] v) {
+ v[0] = (float) mClampedEotf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedEotf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedEotf.applyAsDouble(v[2]);
+ return v;
+ }
+
+ /**
+ * <p>Encodes an RGB value from linear space to this color space's
+ * "gamma space". This is achieved by applying this color space's
+ * opto-electronic transfer function to the supplied values.</p>
+ *
+ * <p>Refer to the documentation of {@link ColorSpace.Rgb} for
+ * more information about transfer functions and their use for
+ * encoding and decoding RGB values.</p>
+ *
+ * @param r The red component to encode from linear space
+ * @param g The green component to encode from linear space
+ * @param b The blue component to encode from linear space
+ * @return A new array of 3 floats containing non-linear RGB values
+ *
+ * @see #fromLinear(float[])
+ * @see #toLinear(float, float, float)
+ */
+ @NonNull
+ @Size(3)
+ public float[] fromLinear(float r, float g, float b) {
+ return fromLinear(new float[] { r, g, b });
+ }
+
+ /**
+ * <p>Encodes an RGB value from linear space to this color space's
+ * "gamma space". This is achieved by applying this color space's
+ * opto-electronic transfer function to the first 3 values of the
+ * supplied array. The result is stored back in the input array.</p>
+ *
+ * <p>Refer to the documentation of {@link ColorSpace.Rgb} for
+ * more information about transfer functions and their use for
+ * encoding and decoding RGB values.</p>
+ *
+ * @param v A non-null array of linear RGB values, its length
+ * must be at least 3
+ * @return A new array of 3 floats containing non-linear RGB values
+ *
+ * @see #fromLinear(float[])
+ * @see #toLinear(float, float, float)
+ */
+ @NonNull
+ @Size(min = 3)
+ public float[] fromLinear(@NonNull @Size(min = 3) float[] v) {
+ v[0] = (float) mClampedOetf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedOetf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedOetf.applyAsDouble(v[2]);
+ return v;
+ }
+
+ @Override
+ @NonNull
+ @Size(min = 3)
+ public float[] toXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = (float) mClampedEotf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedEotf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedEotf.applyAsDouble(v[2]);
+ return mul3x3Float3(mTransform, v);
+ }
+
+ @Override
+ @NonNull
+ @Size(min = 3)
+ public float[] fromXyz(@NonNull @Size(min = 3) float[] v) {
+ mul3x3Float3(mInverseTransform, v);
+ v[0] = (float) mClampedOetf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedOetf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedOetf.applyAsDouble(v[2]);
+ return v;
+ }
+
+ private double clamp(double x) {
+ return x < mMin ? mMin : x > mMax ? mMax : x;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ Rgb rgb = (Rgb) o;
+
+ if (Float.compare(rgb.mMin, mMin) != 0) return false;
+ if (Float.compare(rgb.mMax, mMax) != 0) return false;
+ if (!Arrays.equals(mWhitePoint, rgb.mWhitePoint)) return false;
+ if (!Arrays.equals(mPrimaries, rgb.mPrimaries)) return false;
+ if (mTransferParameters != null) {
+ return mTransferParameters.equals(rgb.mTransferParameters);
+ } else if (rgb.mTransferParameters == null) {
+ return true;
+ }
+ //noinspection SimplifiableIfStatement
+ if (!mOetf.equals(rgb.mOetf)) return false;
+ return mEotf.equals(rgb.mEotf);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + Arrays.hashCode(mWhitePoint);
+ result = 31 * result + Arrays.hashCode(mPrimaries);
+ result = 31 * result + (mMin != +0.0f ? Float.floatToIntBits(mMin) : 0);
+ result = 31 * result + (mMax != +0.0f ? Float.floatToIntBits(mMax) : 0);
+ result = 31 * result +
+ (mTransferParameters != null ? mTransferParameters.hashCode() : 0);
+ if (mTransferParameters == null) {
+ result = 31 * result + mOetf.hashCode();
+ result = 31 * result + mEotf.hashCode();
+ }
+ return result;
+ }
+
+ /**
+ * Computes whether a color space is the sRGB color space or at least
+ * a close approximation.
+ *
+ * @param primaries The set of RGB primaries in xyY as an array of 6 floats
+ * @param whitePoint The white point in xyY as an array of 2 floats
+ * @param OETF The opto-electronic transfer function
+ * @param EOTF The electro-optical transfer function
+ * @param min The minimum value of the color space's range
+ * @param max The minimum value of the color space's range
+ * @param id The ID of the color space
+ * @return True if the color space can be considered as the sRGB color space
+ *
+ * @see #isSrgb()
+ */
+ @SuppressWarnings("RedundantIfStatement")
+ private static boolean isSrgb(
+ @NonNull @Size(6) float[] primaries,
+ @NonNull @Size(2) float[] whitePoint,
+ @NonNull DoubleUnaryOperator OETF,
+ @NonNull DoubleUnaryOperator EOTF,
+ float min,
+ float max,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ if (id == 0) return true;
+ if (!compare(primaries, SRGB_PRIMARIES)) {
+ return false;
+ }
+ if (!compare(whitePoint, ILLUMINANT_D65)) {
+ return false;
+ }
+ if (OETF.applyAsDouble(0.5) < 0.5001) return false;
+ if (EOTF.applyAsDouble(0.5) > 0.5001) return false;
+ if (min != 0.0f) return false;
+ if (max != 1.0f) return false;
+ return true;
+ }
+
+ /**
+ * Computes whether the specified CIE xyY or XYZ primaries (with Y set to 1) form
+ * a wide color gamut. A color gamut is considered wide if its area is > 90%
+ * of the area of NTSC 1953 and if it contains the sRGB color gamut entirely.
+ * If the conditions above are not met, the color space is considered as having
+ * a wide color gamut if its range is larger than [0..1].
+ *
+ * @param primaries RGB primaries in CIE xyY as an array of 6 floats
+ * @param min The minimum value of the color space's range
+ * @param max The minimum value of the color space's range
+ * @return True if the color space has a wide gamut, false otherwise
+ *
+ * @see #isWideGamut()
+ * @see #area(float[])
+ */
+ private static boolean isWideGamut(@NonNull @Size(6) float[] primaries,
+ float min, float max) {
+ return (area(primaries) / area(NTSC_1953_PRIMARIES) > 0.9f &&
+ contains(primaries, SRGB_PRIMARIES)) || (min < 0.0f && max > 1.0f);
+ }
+
+ /**
+ * Computes the area of the triangle represented by a set of RGB primaries
+ * in the CIE xyY space.
+ *
+ * @param primaries The triangle's vertices, as RGB primaries in an array of 6 floats
+ * @return The area of the triangle
+ *
+ * @see #isWideGamut(float[], float, float)
+ */
+ private static float area(@NonNull @Size(6) float[] primaries) {
+ float Rx = primaries[0];
+ float Ry = primaries[1];
+ float Gx = primaries[2];
+ float Gy = primaries[3];
+ float Bx = primaries[4];
+ float By = primaries[5];
+ float det = Rx * Gy + Ry * Bx + Gx * By - Gy * Bx - Ry * Gx - Rx * By;
+ float r = 0.5f * det;
+ return r < 0.0f ? -r : r;
+ }
+
+ /**
+ * Computes the cross product of two 2D vectors.
+ *
+ * @param ax The x coordinate of the first vector
+ * @param ay The y coordinate of the first vector
+ * @param bx The x coordinate of the second vector
+ * @param by The y coordinate of the second vector
+ * @return The result of a x b
+ */
+ private static float cross(float ax, float ay, float bx, float by) {
+ return ax * by - ay * bx;
+ }
+
+ /**
+ * Decides whether a 2D triangle, identified by the 6 coordinates of its
+ * 3 vertices, is contained within another 2D triangle, also identified
+ * by the 6 coordinates of its 3 vertices.
+ *
+ * In the illustration below, we want to test whether the RGB triangle
+ * is contained within the triangle XYZ formed by the 3 vertices at
+ * the "+" locations.
+ *
+ * Y .
+ * . + .
+ * . ..
+ * . .
+ * . .
+ * . G
+ * *
+ * * *
+ * ** *
+ * * **
+ * * *
+ * ** *
+ * * *
+ * * *
+ * ** *
+ * * *
+ * * **
+ * ** * R ...
+ * * * .....
+ * * ***** ..
+ * ** ************ . +
+ * B * ************ . X
+ * ......***** .
+ * ...... . .
+ * ..
+ * + .
+ * Z .
+ *
+ * RGB is contained within XYZ if all the following conditions are true
+ * (with "x" the cross product operator):
+ *
+ * --> -->
+ * GR x RX >= 0
+ * --> -->
+ * RX x BR >= 0
+ * --> -->
+ * RG x GY >= 0
+ * --> -->
+ * GY x RG >= 0
+ * --> -->
+ * RB x BZ >= 0
+ * --> -->
+ * BZ x GB >= 0
+ *
+ * @param p1 The enclosing triangle
+ * @param p2 The enclosed triangle
+ * @return True if the triangle p1 contains the triangle p2
+ *
+ * @see #isWideGamut(float[], float, float)
+ */
+ @SuppressWarnings("RedundantIfStatement")
+ private static boolean contains(@NonNull @Size(6) float[] p1, @NonNull @Size(6) float[] p2) {
+ // Translate the vertices p1 in the coordinates system
+ // with the vertices p2 as the origin
+ float[] p0 = new float[] {
+ p1[0] - p2[0], p1[1] - p2[1],
+ p1[2] - p2[2], p1[3] - p2[3],
+ p1[4] - p2[4], p1[5] - p2[5],
+ };
+ // Check the first vertex of p1
+ if (cross(p0[0], p0[1], p2[0] - p2[4], p2[1] - p2[5]) < 0 ||
+ cross(p2[0] - p2[2], p2[1] - p2[3], p0[0], p0[1]) < 0) {
+ return false;
+ }
+ // Check the second vertex of p1
+ if (cross(p0[2], p0[3], p2[2] - p2[0], p2[3] - p2[1]) < 0 ||
+ cross(p2[2] - p2[4], p2[3] - p2[5], p0[2], p0[3]) < 0) {
+ return false;
+ }
+ // Check the third vertex of p1
+ if (cross(p0[4], p0[5], p2[4] - p2[2], p2[5] - p2[3]) < 0 ||
+ cross(p2[4] - p2[0], p2[5] - p2[1], p0[4], p0[5]) < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Computes the primaries of a color space identified only by
+ * its RGB->XYZ transform matrix. This method assumes that the
+ * range of the color space is [0..1].
+ *
+ * @param toXYZ The color space's 3x3 transform matrix to XYZ
+ * @return A new array of 6 floats containing the color space's
+ * primaries in CIE xyY
+ */
+ @NonNull
+ @Size(6)
+ private static float[] computePrimaries(@NonNull @Size(9) float[] toXYZ) {
+ float[] r = mul3x3Float3(toXYZ, new float[] { 1.0f, 0.0f, 0.0f });
+ float[] g = mul3x3Float3(toXYZ, new float[] { 0.0f, 1.0f, 0.0f });
+ float[] b = mul3x3Float3(toXYZ, new float[] { 0.0f, 0.0f, 1.0f });
+
+ float rSum = r[0] + r[1] + r[2];
+ float gSum = g[0] + g[1] + g[2];
+ float bSum = b[0] + b[1] + b[2];
+
+ return new float[] {
+ r[0] / rSum, r[1] / rSum,
+ g[0] / gSum, g[1] / gSum,
+ b[0] / bSum, b[1] / bSum,
+ };
+ }
+
+ /**
+ * Computes the white point of a color space identified only by
+ * its RGB->XYZ transform matrix. This method assumes that the
+ * range of the color space is [0..1].
+ *
+ * @param toXYZ The color space's 3x3 transform matrix to XYZ
+ * @return A new array of 2 floats containing the color space's
+ * white point in CIE xyY
+ */
+ @NonNull
+ @Size(2)
+ private static float[] computeWhitePoint(@NonNull @Size(9) float[] toXYZ) {
+ float[] w = mul3x3Float3(toXYZ, new float[] { 1.0f, 1.0f, 1.0f });
+ float sum = w[0] + w[1] + w[2];
+ return new float[] { w[0] / sum, w[1] / sum };
+ }
+
+ /**
+ * Converts the specified RGB primaries point to xyY if needed. The primaries
+ * can be specified as an array of 6 floats (in CIE xyY) or 9 floats
+ * (in CIE XYZ). If no conversion is needed, the input array is copied.
+ *
+ * @param primaries The primaries in xyY or XYZ
+ * @return A new array of 6 floats containing the primaries in xyY
+ */
+ @NonNull
+ @Size(6)
+ private static float[] xyPrimaries(@NonNull @Size(min = 6, max = 9) float[] primaries) {
+ float[] xyPrimaries = new float[6];
+
+ // XYZ to xyY
+ if (primaries.length == 9) {
+ float sum;
+
+ sum = primaries[0] + primaries[1] + primaries[2];
+ xyPrimaries[0] = primaries[0] / sum;
+ xyPrimaries[1] = primaries[1] / sum;
+
+ sum = primaries[3] + primaries[4] + primaries[5];
+ xyPrimaries[2] = primaries[3] / sum;
+ xyPrimaries[3] = primaries[4] / sum;
+
+ sum = primaries[6] + primaries[7] + primaries[8];
+ xyPrimaries[4] = primaries[6] / sum;
+ xyPrimaries[5] = primaries[7] / sum;
+ } else {
+ System.arraycopy(primaries, 0, xyPrimaries, 0, 6);
+ }
+
+ return xyPrimaries;
+ }
+
+ /**
+ * Converts the specified white point to xyY if needed. The white point
+ * can be specified as an array of 2 floats (in CIE xyY) or 3 floats
+ * (in CIE XYZ). If no conversion is needed, the input array is copied.
+ *
+ * @param whitePoint The white point in xyY or XYZ
+ * @return A new array of 2 floats containing the white point in xyY
+ */
+ @NonNull
+ @Size(2)
+ private static float[] xyWhitePoint(@Size(min = 2, max = 3) float[] whitePoint) {
+ float[] xyWhitePoint = new float[2];
+
+ // XYZ to xyY
+ if (whitePoint.length == 3) {
+ float sum = whitePoint[0] + whitePoint[1] + whitePoint[2];
+ xyWhitePoint[0] = whitePoint[0] / sum;
+ xyWhitePoint[1] = whitePoint[1] / sum;
+ } else {
+ System.arraycopy(whitePoint, 0, xyWhitePoint, 0, 2);
+ }
+
+ return xyWhitePoint;
+ }
+
+ /**
+ * Computes the matrix that converts from RGB to XYZ based on RGB
+ * primaries and a white point, both specified in the CIE xyY space.
+ * The Y component of the primaries and white point is implied to be 1.
+ *
+ * @param primaries The RGB primaries in xyY, as an array of 6 floats
+ * @param whitePoint The white point in xyY, as an array of 2 floats
+ * @return A 3x3 matrix as a new array of 9 floats
+ */
+ @NonNull
+ @Size(9)
+ private static float[] computeXYZMatrix(
+ @NonNull @Size(6) float[] primaries,
+ @NonNull @Size(2) float[] whitePoint) {
+ float Rx = primaries[0];
+ float Ry = primaries[1];
+ float Gx = primaries[2];
+ float Gy = primaries[3];
+ float Bx = primaries[4];
+ float By = primaries[5];
+ float Wx = whitePoint[0];
+ float Wy = whitePoint[1];
+
+ float oneRxRy = (1 - Rx) / Ry;
+ float oneGxGy = (1 - Gx) / Gy;
+ float oneBxBy = (1 - Bx) / By;
+ float oneWxWy = (1 - Wx) / Wy;
+
+ float RxRy = Rx / Ry;
+ float GxGy = Gx / Gy;
+ float BxBy = Bx / By;
+ float WxWy = Wx / Wy;
+
+ float BY =
+ ((oneWxWy - oneRxRy) * (GxGy - RxRy) - (WxWy - RxRy) * (oneGxGy - oneRxRy)) /
+ ((oneBxBy - oneRxRy) * (GxGy - RxRy) - (BxBy - RxRy) * (oneGxGy - oneRxRy));
+ float GY = (WxWy - RxRy - BY * (BxBy - RxRy)) / (GxGy - RxRy);
+ float RY = 1 - GY - BY;
+
+ float RYRy = RY / Ry;
+ float GYGy = GY / Gy;
+ float BYBy = BY / By;
+
+ return new float[] {
+ RYRy * Rx, RY, RYRy * (1 - Rx - Ry),
+ GYGy * Gx, GY, GYGy * (1 - Gx - Gy),
+ BYBy * Bx, BY, BYBy * (1 - Bx - By)
+ };
+ }
+ }
+
+ /**
+ * {@usesMathJax}
+ *
+ * <p>A connector transforms colors from a source color space to a destination
+ * color space.</p>
+ *
+ * <p>A source color space is connected to a destination color space using the
+ * color transform \(C\) computed from their respective transforms noted
+ * \(T_{src}\) and \(T_{dst}\) in the following equation:</p>
+ *
+ * $$C = T^{-1}_{dst} . T_{src}$$
+ *
+ * <p>The transform \(C\) shown above is only valid when the source and
+ * destination color spaces have the same profile connection space (PCS).
+ * We know that instances of {@link ColorSpace} always use CIE XYZ as their
+ * PCS but their white points might differ. When they do, we must perform
+ * a chromatic adaptation of the color spaces' transforms. To do so, we
+ * use the von Kries method described in the documentation of {@link Adaptation},
+ * using the CIE standard illuminant {@link ColorSpace#ILLUMINANT_D50 D50}
+ * as the target white point.</p>
+ *
+ * <p>Example of conversion from {@link Named#SRGB sRGB} to
+ * {@link Named#DCI_P3 DCI-P3}:</p>
+ *
+ * <pre class="prettyprint">
+ * ColorSpace.Connector connector = ColorSpace.connect(
+ * ColorSpace.get(ColorSpace.Named.SRGB),
+ * ColorSpace.get(ColorSpace.Named.DCI_P3));
+ * float[] p3 = connector.transform(1.0f, 0.0f, 0.0f);
+ * // p3 contains { 0.9473, 0.2740, 0.2076 }
+ * </pre>
+ *
+ * @see Adaptation
+ * @see ColorSpace#adapt(ColorSpace, float[], Adaptation)
+ * @see ColorSpace#adapt(ColorSpace, float[])
+ * @see ColorSpace#connect(ColorSpace, ColorSpace, RenderIntent)
+ * @see ColorSpace#connect(ColorSpace, ColorSpace)
+ * @see ColorSpace#connect(ColorSpace, RenderIntent)
+ * @see ColorSpace#connect(ColorSpace)
+ */
+ @AnyThread
+ public static class Connector {
+ @NonNull private final ColorSpace mSource;
+ @NonNull private final ColorSpace mDestination;
+ @NonNull private final ColorSpace mTransformSource;
+ @NonNull private final ColorSpace mTransformDestination;
+ @NonNull private final RenderIntent mIntent;
+ @NonNull @Size(3) private final float[] mTransform;
+
+ /**
+ * Creates a new connector between a source and a destination color space.
+ *
+ * @param source The source color space, cannot be null
+ * @param destination The destination color space, cannot be null
+ * @param intent The render intent to use when compressing gamuts
+ */
+ Connector(@NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull RenderIntent intent) {
+ this(source, destination,
+ source.getModel() == Model.RGB ? adapt(source, ILLUMINANT_D50_XYZ) : source,
+ destination.getModel() == Model.RGB ?
+ adapt(destination, ILLUMINANT_D50_XYZ) : destination,
+ intent, computeTransform(source, destination, intent));
+ }
+
+ /**
+ * To connect between color spaces, we might need to use adapted transforms.
+ * This should be transparent to the user so this constructor takes the
+ * original source and destinations (returned by the getters), as well as
+ * possibly adapted color spaces used by transform().
+ */
+ private Connector(
+ @NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull ColorSpace transformSource, @NonNull ColorSpace transformDestination,
+ @NonNull RenderIntent intent, @Nullable @Size(3) float[] transform) {
+ mSource = source;
+ mDestination = destination;
+ mTransformSource = transformSource;
+ mTransformDestination = transformDestination;
+ mIntent = intent;
+ mTransform = transform;
+ }
+
+ /**
+ * Computes an extra transform to apply in XYZ space depending on the
+ * selected rendering intent.
+ */
+ @Nullable
+ private static float[] computeTransform(@NonNull ColorSpace source,
+ @NonNull ColorSpace destination, @NonNull RenderIntent intent) {
+ if (intent != RenderIntent.ABSOLUTE) return null;
+
+ boolean srcRGB = source.getModel() == Model.RGB;
+ boolean dstRGB = destination.getModel() == Model.RGB;
+
+ if (srcRGB && dstRGB) return null;
+
+ if (srcRGB || dstRGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) (srcRGB ? source : destination);
+ float[] srcXYZ = srcRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ;
+ float[] dstXYZ = dstRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ;
+ return new float[] {
+ srcXYZ[0] / dstXYZ[0],
+ srcXYZ[1] / dstXYZ[1],
+ srcXYZ[2] / dstXYZ[2],
+ };
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the source color space this connector will convert from.
+ *
+ * @return A non-null instance of {@link ColorSpace}
+ *
+ * @see #getDestination()
+ */
+ @NonNull
+ public ColorSpace getSource() {
+ return mSource;
+ }
+
+ /**
+ * Returns the destination color space this connector will convert to.
+ *
+ * @return A non-null instance of {@link ColorSpace}
+ *
+ * @see #getSource()
+ */
+ @NonNull
+ public ColorSpace getDestination() {
+ return mDestination;
+ }
+
+ /**
+ * Returns the render intent this connector will use when mapping the
+ * source color space to the destination color space.
+ *
+ * @return A non-null {@link RenderIntent}
+ *
+ * @see RenderIntent
+ */
+ public RenderIntent getRenderIntent() {
+ return mIntent;
+ }
+
+ /**
+ * <p>Transforms the specified color from the source color space
+ * to a color in the destination color space. This convenience
+ * method assumes a source color model with 3 components
+ * (typically RGB). To transform from color models with more than
+ * 3 components, such as {@link Model#CMYK CMYK}, use
+ * {@link #transform(float[])} instead.</p>
+ *
+ * @param r The red component of the color to transform
+ * @param g The green component of the color to transform
+ * @param b The blue component of the color to transform
+ * @return A new array of 3 floats containing the specified color
+ * transformed from the source space to the destination space
+ *
+ * @see #transform(float[])
+ */
+ @NonNull
+ @Size(3)
+ public float[] transform(float r, float g, float b) {
+ return transform(new float[] { r, g, b });
+ }
+
+ /**
+ * <p>Transforms the specified color from the source color space
+ * to a color in the destination color space.</p>
+ *
+ * @param v A non-null array of 3 floats containing the value to transform
+ * and that will hold the result of the transform
+ * @return The v array passed as a parameter, containing the specified color
+ * transformed from the source space to the destination space
+ *
+ * @see #transform(float, float, float)
+ */
+ @NonNull
+ @Size(min = 3)
+ public float[] transform(@NonNull @Size(min = 3) float[] v) {
+ float[] xyz = mTransformSource.toXyz(v);
+ if (mTransform != null) {
+ xyz[0] *= mTransform[0];
+ xyz[1] *= mTransform[1];
+ xyz[2] *= mTransform[2];
+ }
+ return mTransformDestination.fromXyz(xyz);
+ }
+
+ /**
+ * Optimized connector for RGB->RGB conversions.
+ */
+ private static class Rgb extends Connector {
+ @NonNull private final ColorSpace.Rgb mSource;
+ @NonNull private final ColorSpace.Rgb mDestination;
+ @NonNull private final float[] mTransform;
+
+ Rgb(@NonNull ColorSpace.Rgb source, @NonNull ColorSpace.Rgb destination,
+ @NonNull RenderIntent intent) {
+ super(source, destination, source, destination, intent, null);
+ mSource = source;
+ mDestination = destination;
+ mTransform = computeTransform(source, destination, intent);
+ }
+
+ @Override
+ public float[] transform(@NonNull @Size(min = 3) float[] rgb) {
+ rgb[0] = (float) mSource.mClampedEotf.applyAsDouble(rgb[0]);
+ rgb[1] = (float) mSource.mClampedEotf.applyAsDouble(rgb[1]);
+ rgb[2] = (float) mSource.mClampedEotf.applyAsDouble(rgb[2]);
+ mul3x3Float3(mTransform, rgb);
+ rgb[0] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[0]);
+ rgb[1] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[1]);
+ rgb[2] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[2]);
+ return rgb;
+ }
+
+ /**
+ * <p>Computes the color transform that connects two RGB color spaces.</p>
+ *
+ * <p>We can only connect color spaces if they use the same profile
+ * connection space. We assume the connection space is always
+ * CIE XYZ but we maye need to perform a chromatic adaptation to
+ * match the white points. If an adaptation is needed, we use the
+ * CIE standard illuminant D50. The unmatched color space is adapted
+ * using the von Kries transform and the {@link Adaptation#BRADFORD}
+ * matrix.</p>
+ *
+ * @param source The source color space, cannot be null
+ * @param destination The destination color space, cannot be null
+ * @param intent The render intent to use when compressing gamuts
+ * @return An array of 9 floats containing the 3x3 matrix transform
+ */
+ @NonNull
+ @Size(9)
+ private static float[] computeTransform(
+ @NonNull ColorSpace.Rgb source,
+ @NonNull ColorSpace.Rgb destination,
+ @NonNull RenderIntent intent) {
+ if (compare(source.mWhitePoint, destination.mWhitePoint)) {
+ // RGB->RGB using the PCS of both color spaces since they have the same
+ return mul3x3(destination.mInverseTransform, source.mTransform);
+ } else {
+ // RGB->RGB using CIE XYZ D50 as the PCS
+ float[] transform = source.mTransform;
+ float[] inverseTransform = destination.mInverseTransform;
+
+ float[] srcXYZ = xyYToXyz(source.mWhitePoint);
+ float[] dstXYZ = xyYToXyz(destination.mWhitePoint);
+
+ if (!compare(source.mWhitePoint, ILLUMINANT_D50)) {
+ float[] srcAdaptation = chromaticAdaptation(
+ Adaptation.BRADFORD.mTransform, srcXYZ,
+ Arrays.copyOf(ILLUMINANT_D50_XYZ, 3));
+ transform = mul3x3(srcAdaptation, source.mTransform);
+ }
+
+ if (!compare(destination.mWhitePoint, ILLUMINANT_D50)) {
+ float[] dstAdaptation = chromaticAdaptation(
+ Adaptation.BRADFORD.mTransform, dstXYZ,
+ Arrays.copyOf(ILLUMINANT_D50_XYZ, 3));
+ inverseTransform = inverse3x3(mul3x3(dstAdaptation, destination.mTransform));
+ }
+
+ if (intent == RenderIntent.ABSOLUTE) {
+ transform = mul3x3Diag(
+ new float[] {
+ srcXYZ[0] / dstXYZ[0],
+ srcXYZ[1] / dstXYZ[1],
+ srcXYZ[2] / dstXYZ[2],
+ }, transform);
+ }
+
+ return mul3x3(inverseTransform, transform);
+ }
+ }
+ }
+
+ /**
+ * Returns the identity connector for a given color space.
+ *
+ * @param source The source and destination color space
+ * @return A non-null connector that does not perform any transform
+ *
+ * @see ColorSpace#connect(ColorSpace, ColorSpace)
+ */
+ static Connector identity(ColorSpace source) {
+ return new Connector(source, source, RenderIntent.RELATIVE) {
+ @Override
+ public float[] transform(@NonNull @Size(min = 3) float[] v) {
+ return v;
+ }
+ };
+ }
+ }
+
+ /**
+ * <p>A color space renderer can be used to visualize and compare the gamut and
+ * white point of one or more color spaces. The output is an sRGB {@link Bitmap}
+ * showing a CIE 1931 xyY or a CIE 1976 UCS chromaticity diagram.</p>
+ *
+ * <p>The following code snippet shows how to compare the {@link Named#SRGB}
+ * and {@link Named#DCI_P3} color spaces in a CIE 1931 diagram:</p>
+ *
+ * <pre class="prettyprint">
+ * Bitmap bitmap = ColorSpace.createRenderer()
+ * .size(768)
+ * .clip(true)
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
+ * .render();
+ * </pre>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_clipped.png" />
+ * <figcaption style="text-align: center;">sRGB vs DCI-P3</figcaption>
+ * </p>
+ *
+ * <p>A renderer can also be used to show the location of specific colors,
+ * associated with a color space, in the CIE 1931 xyY chromaticity diagram.
+ * See {@link #add(ColorSpace, float, float, float, int)} for more information.</p>
+ *
+ * @see ColorSpace#createRenderer()
+ *
+ * @hide
+ */
+ public static class Renderer {
+ private static final int NATIVE_SIZE = 1440;
+ private static final float UCS_SCALE = 9.0f / 6.0f;
+
+ // Number of subdivision of the inside of the spectral locus
+ private static final int CHROMATICITY_RESOLUTION = 32;
+ private static final double ONE_THIRD = 1.0 / 3.0;
+
+ @IntRange(from = 128, to = Integer.MAX_VALUE)
+ private int mSize = 1024;
+
+ private boolean mShowWhitePoint = true;
+ private boolean mClip = false;
+ private boolean mUcs = false;
+
+ private final List<Pair<ColorSpace, Integer>> mColorSpaces = new ArrayList<>(2);
+ private final List<Point> mPoints = new ArrayList<>(0);
+
+ private Renderer() {
+ }
+
+ /**
+ * <p>Defines whether the chromaticity diagram should be clipped by the first
+ * registered color space. The default value is false.</p>
+ *
+ * <p>The following code snippet and image show the default behavior:</p>
+ * <pre class="prettyprint">
+ * Bitmap bitmap = ColorSpace.createRenderer()
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
+ * .render();
+ * </pre>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_comparison.png" />
+ * <figcaption style="text-align: center;">Clipping disabled</figcaption>
+ * </p>
+ *
+ * <p>Here is the same example with clipping enabled:</p>
+ * <pre class="prettyprint">
+ * Bitmap bitmap = ColorSpace.createRenderer()
+ * .clip(true)
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
+ * .render();
+ * </pre>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_clipped.png" />
+ * <figcaption style="text-align: center;">Clipping enabled</figcaption>
+ * </p>
+ *
+ * @param clip True to clip the chromaticity diagram to the first registered color space,
+ * false otherwise
+ * @return This instance of {@link Renderer}
+ */
+ @NonNull
+ public Renderer clip(boolean clip) {
+ mClip = clip;
+ return this;
+ }
+
+ /**
+ * <p>Defines whether the chromaticity diagram should use the uniform
+ * chromaticity scale (CIE 1976 UCS). When the uniform chromaticity scale
+ * is used, the distance between two points on the diagram is approximately
+ * proportional to the perceived color difference.</p>
+ *
+ * <p>The following code snippet shows how to enable the uniform chromaticity
+ * scale. The image below shows the result:</p>
+ * <pre class="prettyprint">
+ * Bitmap bitmap = ColorSpace.createRenderer()
+ * .uniformChromaticityScale(true)
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
+ * .render();
+ * </pre>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_ucs.png" />
+ * <figcaption style="text-align: center;">CIE 1976 UCS diagram</figcaption>
+ * </p>
+ *
+ * @param ucs True to render the chromaticity diagram as the CIE 1976 UCS diagram
+ * @return This instance of {@link Renderer}
+ */
+ @NonNull
+ public Renderer uniformChromaticityScale(boolean ucs) {
+ mUcs = ucs;
+ return this;
+ }
+
+ /**
+ * Sets the dimensions (width and height) in pixels of the output bitmap.
+ * The size must be at least 128px and defaults to 1024px.
+ *
+ * @param size The size in pixels of the output bitmap
+ * @return This instance of {@link Renderer}
+ */
+ @NonNull
+ public Renderer size(@IntRange(from = 128, to = Integer.MAX_VALUE) int size) {
+ mSize = Math.max(128, size);
+ return this;
+ }
+
+ /**
+ * Shows or hides the white point of each color space in the output bitmap.
+ * The default is true.
+ *
+ * @param show True to show the white point of each color space, false
+ * otherwise
+ * @return This instance of {@link Renderer}
+ */
+ @NonNull
+ public Renderer showWhitePoint(boolean show) {
+ mShowWhitePoint = show;
+ return this;
+ }
+
+ /**
+ * <p>Adds a color space to represent on the output CIE 1931 chromaticity
+ * diagram. The color space is represented as a triangle showing the
+ * footprint of its color gamut and, optionally, the location of its
+ * white point.</p>
+ *
+ * <p class="note">Color spaces with a color model that is not RGB are
+ * accepted but ignored.</p>
+ *
+ * <p>The following code snippet and image show an example of calling this
+ * method to compare {@link Named#SRGB sRGB} and {@link Named#DCI_P3 DCI-P3}:</p>
+ * <pre class="prettyprint">
+ * Bitmap bitmap = ColorSpace.createRenderer()
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
+ * .render();
+ * </pre>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_comparison.png" />
+ * <figcaption style="text-align: center;">sRGB vs DCI-P3</figcaption>
+ * </p>
+ *
+ * <p>Adding a color space extending beyond the boundaries of the
+ * spectral locus will alter the size of the diagram within the output
+ * bitmap as shown in this example:</p>
+ * <pre class="prettyprint">
+ * Bitmap bitmap = ColorSpace.createRenderer()
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
+ * .add(ColorSpace.get(ColorSpace.Named.ACES), 0xff097ae9)
+ * .add(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), 0xff000000)
+ * .render();
+ * </pre>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_comparison2.png" />
+ * <figcaption style="text-align: center;">sRGB, DCI-P3, ACES and scRGB</figcaption>
+ * </p>
+ *
+ * @param colorSpace The color space whose gamut to render on the diagram
+ * @param color The sRGB color to use to render the color space's gamut and white point
+ * @return This instance of {@link Renderer}
+ *
+ * @see #clip(boolean)
+ * @see #showWhitePoint(boolean)
+ */
+ @NonNull
+ public Renderer add(@NonNull ColorSpace colorSpace, @ColorInt int color) {
+ mColorSpaces.add(new Pair<>(colorSpace, color));
+ return this;
+ }
+
+ /**
+ * <p>Adds a color to represent as a point on the chromaticity diagram.
+ * The color is associated with a color space which will be used to
+ * perform the conversion to CIE XYZ and compute the location of the point
+ * on the diagram. The point is rendered as a colored circle.</p>
+ *
+ * <p>The following code snippet and image show an example of calling this
+ * method to render the location of several sRGB colors as white circles:</p>
+ * <pre class="prettyprint">
+ * Bitmap bitmap = ColorSpace.createRenderer()
+ * .clip(true)
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.0f, 0.1f, 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.1f, 0.1f, 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.2f, 0.1f, 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.3f, 0.1f, 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.4f, 0.1f, 0xffffffff)
+ * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.5f, 0.1f, 0xffffffff)
+ * .render();
+ * </pre>
+ * <p>
+ * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_points.png" />
+ * <figcaption style="text-align: center;">
+ * Locating colors on the chromaticity diagram
+ * </figcaption>
+ * </p>
+ *
+ * @param colorSpace The color space of the color to locate on the diagram
+ * @param r The first component of the color to locate on the diagram
+ * @param g The second component of the color to locate on the diagram
+ * @param b The third component of the color to locate on the diagram
+ * @param pointColor The sRGB color to use to render the point on the diagram
+ * @return This instance of {@link Renderer}
+ */
+ @NonNull
+ public Renderer add(@NonNull ColorSpace colorSpace, float r, float g, float b,
+ @ColorInt int pointColor) {
+ mPoints.add(new Point(colorSpace, new float[] { r, g, b }, pointColor));
+ return this;
+ }
+
+ /**
+ * <p>Renders the {@link #add(ColorSpace, int) color spaces} and
+ * {@link #add(ColorSpace, float, float, float, int) points} registered
+ * with this renderer. The output bitmap is an sRGB image with the
+ * dimensions specified by calling {@link #size(int)} (1204x1024px by
+ * default).</p>
+ *
+ * @return A new non-null {@link Bitmap} with the dimensions specified
+ * by {@link #size(int)} (1024x1024 by default)
+ */
+ @NonNull
+ public Bitmap render() {
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ Bitmap bitmap = Bitmap.createBitmap(mSize, mSize, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ float[] primaries = new float[6];
+ float[] whitePoint = new float[2];
+
+ int width = NATIVE_SIZE;
+ int height = NATIVE_SIZE;
+
+ Path path = new Path();
+
+ setTransform(canvas, width, height, primaries);
+ drawBox(canvas, width, height, paint, path);
+ setUcsTransform(canvas, height);
+ drawLocus(canvas, width, height, paint, path, primaries);
+ drawGamuts(canvas, width, height, paint, path, primaries, whitePoint);
+ drawPoints(canvas, width, height, paint);
+
+ return bitmap;
+ }
+
+ /**
+ * Draws registered points at their correct position in the xyY coordinates.
+ * Each point is positioned according to its associated color space.
+ *
+ * @param canvas The canvas to transform
+ * @param width Width in pixel of the final image
+ * @param height Height in pixel of the final image
+ * @param paint A pre-allocated paint used to avoid temporary allocations
+ */
+ private void drawPoints(@NonNull Canvas canvas, int width, int height,
+ @NonNull Paint paint) {
+
+ paint.setStyle(Paint.Style.FILL);
+
+ float radius = 4.0f / (mUcs ? UCS_SCALE : 1.0f);
+
+ float[] v = new float[3];
+ float[] xy = new float[2];
+
+ for (final Point point : mPoints) {
+ v[0] = point.mRgb[0];
+ v[1] = point.mRgb[1];
+ v[2] = point.mRgb[2];
+ point.mColorSpace.toXyz(v);
+
+ paint.setColor(point.mColor);
+
+ // XYZ to xyY, assuming Y=1.0, then to L*u*v* if needed
+ float sum = v[0] + v[1] + v[2];
+ xy[0] = v[0] / sum;
+ xy[1] = v[1] / sum;
+ if (mUcs) xyYToUv(xy);
+
+ canvas.drawCircle(width * xy[0], height - height * xy[1], radius, paint);
+ }
+ }
+
+ /**
+ * Draws the color gamuts and white points of all the registered color
+ * spaces. Only color spaces with an RGB color model are rendered, the
+ * others are ignored.
+ *
+ * @param canvas The canvas to transform
+ * @param width Width in pixel of the final image
+ * @param height Height in pixel of the final image
+ * @param paint A pre-allocated paint used to avoid temporary allocations
+ * @param path A pre-allocated path used to avoid temporary allocations
+ * @param primaries A pre-allocated array of 6 floats to avoid temporary allocations
+ * @param whitePoint A pre-allocated array of 2 floats to avoid temporary allocations
+ */
+ private void drawGamuts(
+ @NonNull Canvas canvas, int width, int height,
+ @NonNull Paint paint, @NonNull Path path,
+ @NonNull @Size(6) float[] primaries, @NonNull @Size(2) float[] whitePoint) {
+
+ float radius = 4.0f / (mUcs ? UCS_SCALE : 1.0f);
+
+ for (final Pair<ColorSpace, Integer> item : mColorSpaces) {
+ ColorSpace colorSpace = item.first;
+ int color = item.second;
+
+ if (colorSpace.getModel() != Model.RGB) continue;
+
+ Rgb rgb = (Rgb) colorSpace;
+ getPrimaries(rgb, primaries, mUcs);
+
+ path.rewind();
+ path.moveTo(width * primaries[0], height - height * primaries[1]);
+ path.lineTo(width * primaries[2], height - height * primaries[3]);
+ path.lineTo(width * primaries[4], height - height * primaries[5]);
+ path.close();
+
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(color);
+ canvas.drawPath(path, paint);
+
+ // Draw the white point
+ if (mShowWhitePoint) {
+ rgb.getWhitePoint(whitePoint);
+ if (mUcs) xyYToUv(whitePoint);
+
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(color);
+ canvas.drawCircle(
+ width * whitePoint[0], height - height * whitePoint[1], radius, paint);
+ }
+ }
+ }
+
+ /**
+ * Returns the primaries of the specified RGB color space. This method handles
+ * the special case of the {@link Named#EXTENDED_SRGB} family of color spaces.
+ *
+ * @param rgb The color space whose primaries to extract
+ * @param primaries A pre-allocated array of 6 floats that will hold the result
+ * @param asUcs True if the primaries should be returned in Luv, false for xyY
+ */
+ @NonNull
+ @Size(6)
+ private static void getPrimaries(@NonNull Rgb rgb,
+ @NonNull @Size(6) float[] primaries, boolean asUcs) {
+ // TODO: We should find a better way to handle these cases
+ if (rgb.equals(ColorSpace.get(Named.EXTENDED_SRGB)) ||
+ rgb.equals(ColorSpace.get(Named.LINEAR_EXTENDED_SRGB))) {
+ primaries[0] = 1.41f;
+ primaries[1] = 0.33f;
+ primaries[2] = 0.27f;
+ primaries[3] = 1.24f;
+ primaries[4] = -0.23f;
+ primaries[5] = -0.57f;
+ } else {
+ rgb.getPrimaries(primaries);
+ }
+ if (asUcs) xyYToUv(primaries);
+ }
+
+ /**
+ * Draws the CIE 1931 chromaticity diagram: the spectral locus and its inside.
+ * This method respect the clip parameter.
+ *
+ * @param canvas The canvas to transform
+ * @param width Width in pixel of the final image
+ * @param height Height in pixel of the final image
+ * @param paint A pre-allocated paint used to avoid temporary allocations
+ * @param path A pre-allocated path used to avoid temporary allocations
+ * @param primaries A pre-allocated array of 6 floats to avoid temporary allocations
+ */
+ private void drawLocus(
+ @NonNull Canvas canvas, int width, int height, @NonNull Paint paint,
+ @NonNull Path path, @NonNull @Size(6) float[] primaries) {
+
+ int vertexCount = SPECTRUM_LOCUS_X.length * CHROMATICITY_RESOLUTION * 6;
+ float[] vertices = new float[vertexCount * 2];
+ int[] colors = new int[vertices.length];
+ computeChromaticityMesh(vertices, colors);
+
+ if (mUcs) xyYToUv(vertices);
+ for (int i = 0; i < vertices.length; i += 2) {
+ vertices[i] *= width;
+ vertices[i + 1] = height - vertices[i + 1] * height;
+ }
+
+ // Draw the spectral locus
+ if (mClip && mColorSpaces.size() > 0) {
+ for (final Pair<ColorSpace, Integer> item : mColorSpaces) {
+ ColorSpace colorSpace = item.first;
+ if (colorSpace.getModel() != Model.RGB) continue;
+
+ Rgb rgb = (Rgb) colorSpace;
+ getPrimaries(rgb, primaries, mUcs);
+
+ break;
+ }
+
+ path.rewind();
+ path.moveTo(width * primaries[0], height - height * primaries[1]);
+ path.lineTo(width * primaries[2], height - height * primaries[3]);
+ path.lineTo(width * primaries[4], height - height * primaries[5]);
+ path.close();
+
+ int[] solid = new int[colors.length];
+ Arrays.fill(solid, 0xff6c6c6c);
+ canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0,
+ null, 0, solid, 0, null, 0, 0, paint);
+
+ canvas.save();
+ canvas.clipPath(path);
+
+ canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0,
+ null, 0, colors, 0, null, 0, 0, paint);
+
+ canvas.restore();
+ } else {
+ canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0,
+ null, 0, colors, 0, null, 0, 0, paint);
+ }
+
+ // Draw the non-spectral locus
+ int index = (CHROMATICITY_RESOLUTION - 1) * 12;
+ path.reset();
+ path.moveTo(vertices[index], vertices[index + 1]);
+ for (int x = 2; x < SPECTRUM_LOCUS_X.length; x++) {
+ index += CHROMATICITY_RESOLUTION * 12;
+ path.lineTo(vertices[index], vertices[index + 1]);
+ }
+ path.close();
+
+ paint.setStrokeWidth(4.0f / (mUcs ? UCS_SCALE : 1.0f));
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(0xff000000);
+ canvas.drawPath(path, paint);
+ }
+
+ /**
+ * Draws the diagram box, including borders, tick marks, grid lines
+ * and axis labels.
+ *
+ * @param canvas The canvas to transform
+ * @param width Width in pixel of the final image
+ * @param height Height in pixel of the final image
+ * @param paint A pre-allocated paint used to avoid temporary allocations
+ * @param path A pre-allocated path used to avoid temporary allocations
+ */
+ private void drawBox(@NonNull Canvas canvas, int width, int height, @NonNull Paint paint,
+ @NonNull Path path) {
+
+ int lineCount = 10;
+ float scale = 1.0f;
+ if (mUcs) {
+ lineCount = 7;
+ scale = UCS_SCALE;
+ }
+
+ // Draw the unit grid
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(2.0f);
+ paint.setColor(0xffc0c0c0);
+
+ for (int i = 1; i < lineCount - 1; i++) {
+ float v = i / 10.0f;
+ float x = (width * v) * scale;
+ float y = height - (height * v) * scale;
+
+ canvas.drawLine(0.0f, y, 0.9f * width, y, paint);
+ canvas.drawLine(x, height, x, 0.1f * height, paint);
+ }
+
+ // Draw tick marks
+ paint.setStrokeWidth(4.0f);
+ paint.setColor(0xff000000);
+ for (int i = 1; i < lineCount - 1; i++) {
+ float v = i / 10.0f;
+ float x = (width * v) * scale;
+ float y = height - (height * v) * scale;
+
+ canvas.drawLine(0.0f, y, width / 100.0f, y, paint);
+ canvas.drawLine(x, height, x, height - (height / 100.0f), paint);
+ }
+
+ // Draw the axis labels
+ paint.setStyle(Paint.Style.FILL);
+ paint.setTextSize(36.0f);
+ paint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
+
+ Rect bounds = new Rect();
+ for (int i = 1; i < lineCount - 1; i++) {
+ String text = "0." + i;
+ paint.getTextBounds(text, 0, text.length(), bounds);
+
+ float v = i / 10.0f;
+ float x = (width * v) * scale;
+ float y = height - (height * v) * scale;
+
+ canvas.drawText(text, -0.05f * width + 10, y + bounds.height() / 2.0f, paint);
+ canvas.drawText(text, x - bounds.width() / 2.0f,
+ height + bounds.height() + 16, paint);
+ }
+ paint.setStyle(Paint.Style.STROKE);
+
+ // Draw the diagram box
+ path.moveTo(0.0f, height);
+ path.lineTo(0.9f * width, height);
+ path.lineTo(0.9f * width, 0.1f * height);
+ path.lineTo(0.0f, 0.1f * height);
+ path.close();
+ canvas.drawPath(path, paint);
+ }
+
+ /**
+ * Computes and applies the Canvas transforms required to make the color
+ * gamut of each color space visible in the final image.
+ *
+ * @param canvas The canvas to transform
+ * @param width Width in pixel of the final image
+ * @param height Height in pixel of the final image
+ * @param primaries Array of 6 floats used to avoid temporary allocations
+ */
+ private void setTransform(@NonNull Canvas canvas, int width, int height,
+ @NonNull @Size(6) float[] primaries) {
+
+ RectF primariesBounds = new RectF();
+ for (final Pair<ColorSpace, Integer> item : mColorSpaces) {
+ ColorSpace colorSpace = item.first;
+ if (colorSpace.getModel() != Model.RGB) continue;
+
+ Rgb rgb = (Rgb) colorSpace;
+ getPrimaries(rgb, primaries, mUcs);
+
+ primariesBounds.left = Math.min(primariesBounds.left, primaries[4]);
+ primariesBounds.top = Math.min(primariesBounds.top, primaries[5]);
+ primariesBounds.right = Math.max(primariesBounds.right, primaries[0]);
+ primariesBounds.bottom = Math.max(primariesBounds.bottom, primaries[3]);
+ }
+
+ float max = mUcs ? 0.6f : 0.9f;
+
+ primariesBounds.left = Math.min(0.0f, primariesBounds.left);
+ primariesBounds.top = Math.min(0.0f, primariesBounds.top);
+ primariesBounds.right = Math.max(max, primariesBounds.right);
+ primariesBounds.bottom = Math.max(max, primariesBounds.bottom);
+
+ float scaleX = max / primariesBounds.width();
+ float scaleY = max / primariesBounds.height();
+ float scale = Math.min(scaleX, scaleY);
+
+ canvas.scale(mSize / (float) NATIVE_SIZE, mSize / (float) NATIVE_SIZE);
+ canvas.scale(scale, scale);
+ canvas.translate(
+ (primariesBounds.width() - max) * width / 2.0f,
+ (primariesBounds.height() - max) * height / 2.0f);
+
+ // The spectrum extends ~0.85 vertically and ~0.65 horizontally
+ // We shift the canvas a little bit to get nicer margins
+ canvas.translate(0.05f * width, -0.05f * height);
+ }
+
+ /**
+ * Computes and applies the Canvas transforms required to render the CIE
+ * 197 UCS chromaticity diagram.
+ *
+ * @param canvas The canvas to transform
+ * @param height Height in pixel of the final image
+ */
+ private void setUcsTransform(@NonNull Canvas canvas, int height) {
+ if (mUcs) {
+ canvas.translate(0.0f, (height - height * UCS_SCALE));
+ canvas.scale(UCS_SCALE, UCS_SCALE);
+ }
+ }
+
+ // X coordinates of the spectral locus in CIE 1931
+ private static final float[] SPECTRUM_LOCUS_X = {
+ 0.175596f, 0.172787f, 0.170806f, 0.170085f, 0.160343f,
+ 0.146958f, 0.139149f, 0.133536f, 0.126688f, 0.115830f,
+ 0.109616f, 0.099146f, 0.091310f, 0.078130f, 0.068717f,
+ 0.054675f, 0.040763f, 0.027497f, 0.016270f, 0.008169f,
+ 0.004876f, 0.003983f, 0.003859f, 0.004646f, 0.007988f,
+ 0.013870f, 0.022244f, 0.027273f, 0.032820f, 0.038851f,
+ 0.045327f, 0.052175f, 0.059323f, 0.066713f, 0.074299f,
+ 0.089937f, 0.114155f, 0.138695f, 0.154714f, 0.192865f,
+ 0.229607f, 0.265760f, 0.301588f, 0.337346f, 0.373083f,
+ 0.408717f, 0.444043f, 0.478755f, 0.512467f, 0.544767f,
+ 0.575132f, 0.602914f, 0.627018f, 0.648215f, 0.665746f,
+ 0.680061f, 0.691487f, 0.700589f, 0.707901f, 0.714015f,
+ 0.719017f, 0.723016f, 0.734674f, 0.717203f, 0.699732f,
+ 0.682260f, 0.664789f, 0.647318f, 0.629847f, 0.612376f,
+ 0.594905f, 0.577433f, 0.559962f, 0.542491f, 0.525020f,
+ 0.507549f, 0.490077f, 0.472606f, 0.455135f, 0.437664f,
+ 0.420193f, 0.402721f, 0.385250f, 0.367779f, 0.350308f,
+ 0.332837f, 0.315366f, 0.297894f, 0.280423f, 0.262952f,
+ 0.245481f, 0.228010f, 0.210538f, 0.193067f, 0.175596f
+ };
+ // Y coordinates of the spectral locus in CIE 1931
+ private static final float[] SPECTRUM_LOCUS_Y = {
+ 0.005295f, 0.004800f, 0.005472f, 0.005976f, 0.014496f,
+ 0.026643f, 0.035211f, 0.042704f, 0.053441f, 0.073601f,
+ 0.086866f, 0.112037f, 0.132737f, 0.170464f, 0.200773f,
+ 0.254155f, 0.317049f, 0.387997f, 0.463035f, 0.538504f,
+ 0.587196f, 0.610526f, 0.654897f, 0.675970f, 0.715407f,
+ 0.750246f, 0.779682f, 0.792153f, 0.802971f, 0.812059f,
+ 0.819430f, 0.825200f, 0.829460f, 0.832306f, 0.833833f,
+ 0.833316f, 0.826231f, 0.814796f, 0.805884f, 0.781648f,
+ 0.754347f, 0.724342f, 0.692326f, 0.658867f, 0.624470f,
+ 0.589626f, 0.554734f, 0.520222f, 0.486611f, 0.454454f,
+ 0.424252f, 0.396516f, 0.372510f, 0.351413f, 0.334028f,
+ 0.319765f, 0.308359f, 0.299317f, 0.292044f, 0.285945f,
+ 0.280951f, 0.276964f, 0.265326f, 0.257200f, 0.249074f,
+ 0.240948f, 0.232822f, 0.224696f, 0.216570f, 0.208444f,
+ 0.200318f, 0.192192f, 0.184066f, 0.175940f, 0.167814f,
+ 0.159688f, 0.151562f, 0.143436f, 0.135311f, 0.127185f,
+ 0.119059f, 0.110933f, 0.102807f, 0.094681f, 0.086555f,
+ 0.078429f, 0.070303f, 0.062177f, 0.054051f, 0.045925f,
+ 0.037799f, 0.029673f, 0.021547f, 0.013421f, 0.005295f
+ };
+
+ /**
+ * Computes a 2D mesh representation of the CIE 1931 chromaticity
+ * diagram.
+ *
+ * @param vertices Array of floats that will hold the mesh vertices
+ * @param colors Array of floats that will hold the mesh colors
+ */
+ private static void computeChromaticityMesh(@NonNull float[] vertices,
+ @NonNull int[] colors) {
+
+ ColorSpace colorSpace = get(Named.SRGB);
+
+ float[] color = new float[3];
+
+ int vertexIndex = 0;
+ int colorIndex = 0;
+
+ for (int x = 0; x < SPECTRUM_LOCUS_X.length; x++) {
+ int nextX = (x % (SPECTRUM_LOCUS_X.length - 1)) + 1;
+
+ float a1 = (float) Math.atan2(
+ SPECTRUM_LOCUS_Y[x] - ONE_THIRD,
+ SPECTRUM_LOCUS_X[x] - ONE_THIRD);
+ float a2 = (float) Math.atan2(
+ SPECTRUM_LOCUS_Y[nextX] - ONE_THIRD,
+ SPECTRUM_LOCUS_X[nextX] - ONE_THIRD);
+
+ float radius1 = (float) Math.pow(
+ sqr(SPECTRUM_LOCUS_X[x] - ONE_THIRD) +
+ sqr(SPECTRUM_LOCUS_Y[x] - ONE_THIRD),
+ 0.5);
+ float radius2 = (float) Math.pow(
+ sqr(SPECTRUM_LOCUS_X[nextX] - ONE_THIRD) +
+ sqr(SPECTRUM_LOCUS_Y[nextX] - ONE_THIRD),
+ 0.5);
+
+ // Compute patches; each patch is a quad with a different
+ // color associated with each vertex
+ for (int c = 1; c <= CHROMATICITY_RESOLUTION; c++) {
+ float f1 = c / (float) CHROMATICITY_RESOLUTION;
+ float f2 = (c - 1) / (float) CHROMATICITY_RESOLUTION;
+
+ double cr1 = radius1 * Math.cos(a1);
+ double sr1 = radius1 * Math.sin(a1);
+ double cr2 = radius2 * Math.cos(a2);
+ double sr2 = radius2 * Math.sin(a2);
+
+ // Compute the XYZ coordinates of the 4 vertices of the patch
+ float v1x = (float) (ONE_THIRD + cr1 * f1);
+ float v1y = (float) (ONE_THIRD + sr1 * f1);
+ float v1z = 1 - v1x - v1y;
+
+ float v2x = (float) (ONE_THIRD + cr1 * f2);
+ float v2y = (float) (ONE_THIRD + sr1 * f2);
+ float v2z = 1 - v2x - v2y;
+
+ float v3x = (float) (ONE_THIRD + cr2 * f2);
+ float v3y = (float) (ONE_THIRD + sr2 * f2);
+ float v3z = 1 - v3x - v3y;
+
+ float v4x = (float) (ONE_THIRD + cr2 * f1);
+ float v4y = (float) (ONE_THIRD + sr2 * f1);
+ float v4z = 1 - v4x - v4y;
+
+ // Compute the sRGB representation of each XYZ coordinate of the patch
+ colors[colorIndex ] = computeColor(color, v1x, v1y, v1z, colorSpace);
+ colors[colorIndex + 1] = computeColor(color, v2x, v2y, v2z, colorSpace);
+ colors[colorIndex + 2] = computeColor(color, v3x, v3y, v3z, colorSpace);
+ colors[colorIndex + 3] = colors[colorIndex];
+ colors[colorIndex + 4] = colors[colorIndex + 2];
+ colors[colorIndex + 5] = computeColor(color, v4x, v4y, v4z, colorSpace);
+ colorIndex += 6;
+
+ // Flip the mesh upside down to match Canvas' coordinates system
+ vertices[vertexIndex++] = v1x;
+ vertices[vertexIndex++] = v1y;
+ vertices[vertexIndex++] = v2x;
+ vertices[vertexIndex++] = v2y;
+ vertices[vertexIndex++] = v3x;
+ vertices[vertexIndex++] = v3y;
+ vertices[vertexIndex++] = v1x;
+ vertices[vertexIndex++] = v1y;
+ vertices[vertexIndex++] = v3x;
+ vertices[vertexIndex++] = v3y;
+ vertices[vertexIndex++] = v4x;
+ vertices[vertexIndex++] = v4y;
+ }
+ }
+ }
+
+ @ColorInt
+ private static int computeColor(@NonNull @Size(3) float[] color,
+ float x, float y, float z, @NonNull ColorSpace cs) {
+ color[0] = x;
+ color[1] = y;
+ color[2] = z;
+ cs.fromXyz(color);
+ return 0xff000000 |
+ (((int) (color[0] * 255.0f) & 0xff) << 16) |
+ (((int) (color[1] * 255.0f) & 0xff) << 8) |
+ (((int) (color[2] * 255.0f) & 0xff) );
+ }
+
+ private static double sqr(double v) {
+ return v * v;
+ }
+
+ private static class Point {
+ @NonNull final ColorSpace mColorSpace;
+ @NonNull final float[] mRgb;
+ final int mColor;
+
+ Point(@NonNull ColorSpace colorSpace,
+ @NonNull @Size(3) float[] rgb, @ColorInt int color) {
+ mColorSpace = colorSpace;
+ mRgb = rgb;
+ mColor = color;
+ }
+ }
+ }
+}
diff --git a/android/graphics/ComposePathEffect.java b/android/graphics/ComposePathEffect.java
new file mode 100644
index 0000000..3fc9eb5
--- /dev/null
+++ b/android/graphics/ComposePathEffect.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+public class ComposePathEffect extends PathEffect {
+
+ /**
+ * Construct a PathEffect whose effect is to apply first the inner effect
+ * and the the outer pathEffect (e.g. outer(inner(path))).
+ */
+ public ComposePathEffect(PathEffect outerpe, PathEffect innerpe) {
+ native_instance = nativeCreate(outerpe.native_instance,
+ innerpe.native_instance);
+ }
+
+ private static native long nativeCreate(long nativeOuterpe,
+ long nativeInnerpe);
+}
+
diff --git a/android/graphics/ComposePathEffect_Delegate.java b/android/graphics/ComposePathEffect_Delegate.java
new file mode 100644
index 0000000..bc3df7d
--- /dev/null
+++ b/android/graphics/ComposePathEffect_Delegate.java
@@ -0,0 +1,71 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.awt.Stroke;
+
+/**
+ * Delegate implementing the native methods of android.graphics.ComposePathEffect
+ *
+ * Through the layoutlib_create tool, the original native methods of ComposePathEffect have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original ComposePathEffect class.
+ *
+ * Because this extends {@link PathEffect_Delegate}, there's no need to use a {@link DelegateManager},
+ * as all the Shader classes will be added to the manager owned by {@link PathEffect_Delegate}.
+ *
+ * @see PathEffect_Delegate
+ *
+ */
+public class ComposePathEffect_Delegate extends PathEffect_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public Stroke getStroke(Paint_Delegate paint) {
+ // FIXME
+ return null;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return false;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ return "Compose Path Effects are not supported in Layout Preview mode.";
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate(long outerpe, long innerpe) {
+ ComposePathEffect_Delegate newDelegate = new ComposePathEffect_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/ComposeShader.java b/android/graphics/ComposeShader.java
new file mode 100644
index 0000000..70a5f53
--- /dev/null
+++ b/android/graphics/ComposeShader.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+
+import android.annotation.NonNull;
+
+/** A subclass of shader that returns the composition of two other shaders, combined by
+ an {@link android.graphics.Xfermode} subclass.
+*/
+public class ComposeShader extends Shader {
+
+ Shader mShaderA;
+ private long mNativeInstanceShaderA;
+ Shader mShaderB;
+ private long mNativeInstanceShaderB;
+ private int mPorterDuffMode;
+
+ /**
+ * Create a new compose shader, given shaders A, B, and a combining mode.
+ * When the mode is applied, it will be given the result from shader A as its
+ * "dst", and the result from shader B as its "src".
+ *
+ * @param shaderA The colors from this shader are seen as the "dst" by the mode
+ * @param shaderB The colors from this shader are seen as the "src" by the mode
+ * @param mode The mode that combines the colors from the two shaders. If mode
+ * is null, then SRC_OVER is assumed.
+ */
+ public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB, @NonNull Xfermode mode) {
+ this(shaderA, shaderB, mode.porterDuffMode);
+ }
+
+ /**
+ * Create a new compose shader, given shaders A, B, and a combining PorterDuff mode.
+ * When the mode is applied, it will be given the result from shader A as its
+ * "dst", and the result from shader B as its "src".
+ *
+ * @param shaderA The colors from this shader are seen as the "dst" by the mode
+ * @param shaderB The colors from this shader are seen as the "src" by the mode
+ * @param mode The PorterDuff mode that combines the colors from the two shaders.
+ */
+ public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB,
+ @NonNull PorterDuff.Mode mode) {
+ this(shaderA, shaderB, mode.nativeInt);
+ }
+
+ private ComposeShader(Shader shaderA, Shader shaderB, int nativeMode) {
+ if (shaderA == null || shaderB == null) {
+ throw new IllegalArgumentException("Shader parameters must not be null");
+ }
+
+ mShaderA = shaderA;
+ mShaderB = shaderB;
+ mPorterDuffMode = nativeMode;
+ }
+
+ @Override
+ long createNativeInstance(long nativeMatrix) {
+ mNativeInstanceShaderA = mShaderA.getNativeInstance();
+ mNativeInstanceShaderB = mShaderB.getNativeInstance();
+ return nativeCreate(nativeMatrix,
+ mShaderA.getNativeInstance(), mShaderB.getNativeInstance(), mPorterDuffMode);
+ }
+
+ /** @hide */
+ @Override
+ protected void verifyNativeInstance() {
+ if (mShaderA.getNativeInstance() != mNativeInstanceShaderA
+ || mShaderB.getNativeInstance() != mNativeInstanceShaderB) {
+ // Child shader native instance has been updated,
+ // so our cached native instance is no longer valid - discard it
+ discardNativeInstance();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected Shader copy() {
+ final ComposeShader copy = new ComposeShader(
+ mShaderA.copy(), mShaderB.copy(), mPorterDuffMode);
+ copyLocalMatrix(copy);
+ return copy;
+ }
+
+ private static native long nativeCreate(long nativeMatrix,
+ long nativeShaderA, long nativeShaderB, int porterDuffMode);
+}
diff --git a/android/graphics/ComposeShader_Delegate.java b/android/graphics/ComposeShader_Delegate.java
new file mode 100644
index 0000000..ab37968
--- /dev/null
+++ b/android/graphics/ComposeShader_Delegate.java
@@ -0,0 +1,79 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.awt.Paint;
+
+/**
+ * Delegate implementing the native methods of android.graphics.ComposeShader
+ *
+ * Through the layoutlib_create tool, the original native methods of ComposeShader have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original ComposeShader class.
+ *
+ * Because this extends {@link Shader_Delegate}, there's no need to use a {@link DelegateManager},
+ * as all the Shader classes will be added to the manager owned by {@link Shader_Delegate}.
+ *
+ * @see Shader_Delegate
+ *
+ */
+public class ComposeShader_Delegate extends Shader_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public Paint getJavaPaint() {
+ // FIXME
+ return null;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return false;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ return "Compose Shaders are not supported in Layout Preview mode.";
+ }
+
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate(long nativeMatrix, long native_shaderA,
+ long native_shaderB, int native_mode) {
+ // FIXME not supported yet.
+ ComposeShader_Delegate newDelegate = new ComposeShader_Delegate(nativeMatrix);
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+
+ // ---- Private delegate/helper methods ----
+
+ private ComposeShader_Delegate(long nativeMatrix) {
+ super(nativeMatrix);
+ }
+}
diff --git a/android/graphics/CornerPathEffect.java b/android/graphics/CornerPathEffect.java
new file mode 100644
index 0000000..8f4d7d9
--- /dev/null
+++ b/android/graphics/CornerPathEffect.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+public class CornerPathEffect extends PathEffect {
+
+ /**
+ * Transforms geometries that are drawn (either STROKE or FILL styles) by
+ * replacing any sharp angles between line segments into rounded angles of
+ * the specified radius.
+ * @param radius Amount to round sharp angles between line segments.
+ */
+ public CornerPathEffect(float radius) {
+ native_instance = nativeCreate(radius);
+ }
+
+ private static native long nativeCreate(float radius);
+}
+
diff --git a/android/graphics/CornerPathEffect_Delegate.java b/android/graphics/CornerPathEffect_Delegate.java
new file mode 100644
index 0000000..73745c3
--- /dev/null
+++ b/android/graphics/CornerPathEffect_Delegate.java
@@ -0,0 +1,71 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.awt.Stroke;
+
+/**
+ * Delegate implementing the native methods of android.graphics.CornerPathEffect
+ *
+ * Through the layoutlib_create tool, the original native methods of CornerPathEffect have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original CornerPathEffect class.
+ *
+ * Because this extends {@link PathEffect_Delegate}, there's no need to use a {@link DelegateManager},
+ * as all the Shader classes will be added to the manager owned by {@link PathEffect_Delegate}.
+ *
+ * @see PathEffect_Delegate
+ *
+ */
+public class CornerPathEffect_Delegate extends PathEffect_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public Stroke getStroke(Paint_Delegate paint) {
+ // FIXME
+ return null;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return false;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ return "Corner Path Effects are not supported in Layout Preview mode.";
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate(float radius) {
+ CornerPathEffect_Delegate newDelegate = new CornerPathEffect_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/DashPathEffect.java b/android/graphics/DashPathEffect.java
new file mode 100644
index 0000000..ef3ebe8
--- /dev/null
+++ b/android/graphics/DashPathEffect.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+public class DashPathEffect extends PathEffect {
+
+ /**
+ * The intervals array must contain an even number of entries (>=2), with
+ * the even indices specifying the "on" intervals, and the odd indices
+ * specifying the "off" intervals. phase is an offset into the intervals
+ * array (mod the sum of all of the intervals). The intervals array
+ * controls the length of the dashes. The paint's strokeWidth controls the
+ * thickness of the dashes.
+ * Note: this patheffect only affects drawing with the paint's style is set
+ * to STROKE or FILL_AND_STROKE. It is ignored if the drawing is done with
+ * style == FILL.
+ * @param intervals array of ON and OFF distances
+ * @param phase offset into the intervals array
+ */
+ public DashPathEffect(float intervals[], float phase) {
+ if (intervals.length < 2) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ native_instance = nativeCreate(intervals, phase);
+ }
+
+ private static native long nativeCreate(float intervals[], float phase);
+}
+
diff --git a/android/graphics/DashPathEffect_Delegate.java b/android/graphics/DashPathEffect_Delegate.java
new file mode 100644
index 0000000..881afde
--- /dev/null
+++ b/android/graphics/DashPathEffect_Delegate.java
@@ -0,0 +1,89 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.awt.BasicStroke;
+import java.awt.Stroke;
+
+/**
+ * Delegate implementing the native methods of android.graphics.DashPathEffect
+ *
+ * Through the layoutlib_create tool, the original native methods of DashPathEffect have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original DashPathEffect class.
+ *
+ * Because this extends {@link PathEffect_Delegate}, there's no need to use a
+ * {@link DelegateManager}, as all the PathEffect classes will be added to the manager owned by
+ * {@link PathEffect_Delegate}.
+ *
+ * @see PathEffect_Delegate
+ *
+ */
+public final class DashPathEffect_Delegate extends PathEffect_Delegate {
+
+ // ---- delegate data ----
+
+ private final float[] mIntervals;
+ private final float mPhase;
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public Stroke getStroke(Paint_Delegate paint) {
+ return new BasicStroke(
+ paint.getStrokeWidth(),
+ paint.getJavaCap(),
+ paint.getJavaJoin(),
+ paint.getJavaStrokeMiter(),
+ mIntervals,
+ mPhase);
+ }
+
+ @Override
+ public boolean isSupported() {
+ return true;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ // no message since isSupported returns true;
+ return null;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate(float intervals[], float phase) {
+ DashPathEffect_Delegate newDelegate = new DashPathEffect_Delegate(intervals, phase);
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+
+ private DashPathEffect_Delegate(float intervals[], float phase) {
+ mIntervals = new float[intervals.length];
+ System.arraycopy(intervals, 0, mIntervals, 0, intervals.length);
+ mPhase = phase;
+ }
+}
+
diff --git a/android/graphics/DiscretePathEffect.java b/android/graphics/DiscretePathEffect.java
new file mode 100644
index 0000000..3b3c9c9
--- /dev/null
+++ b/android/graphics/DiscretePathEffect.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2008 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 android.graphics;
+
+public class DiscretePathEffect extends PathEffect {
+
+ /**
+ * Chop the path into lines of segmentLength, randomly deviating from the
+ * original path by deviation.
+ */
+ public DiscretePathEffect(float segmentLength, float deviation) {
+ native_instance = nativeCreate(segmentLength, deviation);
+ }
+
+ private static native long nativeCreate(float length, float deviation);
+}
+
diff --git a/android/graphics/DiscretePathEffect_Delegate.java b/android/graphics/DiscretePathEffect_Delegate.java
new file mode 100644
index 0000000..46109f3
--- /dev/null
+++ b/android/graphics/DiscretePathEffect_Delegate.java
@@ -0,0 +1,71 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.awt.Stroke;
+
+/**
+ * Delegate implementing the native methods of android.graphics.DiscretePathEffect
+ *
+ * Through the layoutlib_create tool, the original native methods of DiscretePathEffect have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original DiscretePathEffect class.
+ *
+ * Because this extends {@link PathEffect_Delegate}, there's no need to use a {@link DelegateManager},
+ * as all the Shader classes will be added to the manager owned by {@link PathEffect_Delegate}.
+ *
+ * @see PathEffect_Delegate
+ *
+ */
+public class DiscretePathEffect_Delegate extends PathEffect_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public Stroke getStroke(Paint_Delegate paint) {
+ // FIXME
+ return null;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return false;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ return "Discrete Path Effects are not supported in Layout Preview mode.";
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate(float length, float deviation) {
+ DiscretePathEffect_Delegate newDelegate = new DiscretePathEffect_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/DrawFilter.java b/android/graphics/DrawFilter.java
new file mode 100644
index 0000000..c7fdcb2
--- /dev/null
+++ b/android/graphics/DrawFilter.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2008 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 android.graphics;
+
+/**
+ * A DrawFilter subclass can be installed in a Canvas. When it is present, it
+ * can modify the paint that is used to draw (temporarily). With this, a filter
+ * can disable/enable antialiasing, or change the color for everything this is
+ * drawn.
+ */
+public class DrawFilter {
+
+ /**
+ * this is set by subclasses
+ * @hide
+ */
+ public long mNativeInt;
+
+ protected void finalize() throws Throwable {
+ try {
+ nativeDestructor(mNativeInt);
+ mNativeInt = 0;
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private static native void nativeDestructor(long nativeDrawFilter);
+}
+
diff --git a/android/graphics/DrawFilter_Delegate.java b/android/graphics/DrawFilter_Delegate.java
new file mode 100644
index 0000000..2e10740
--- /dev/null
+++ b/android/graphics/DrawFilter_Delegate.java
@@ -0,0 +1,64 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.DrawFilter
+ *
+ * Through the layoutlib_create tool, the original native methods of DrawFilter have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original DrawFilter class.
+ *
+ * This also serve as a base class for all DrawFilter delegate classes.
+ *
+ * @see DelegateManager
+ *
+ */
+public abstract class DrawFilter_Delegate {
+
+ // ---- delegate manager ----
+ protected static final DelegateManager<DrawFilter_Delegate> sManager =
+ new DelegateManager<DrawFilter_Delegate>(DrawFilter_Delegate.class);
+
+ // ---- delegate helper data ----
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ public static DrawFilter_Delegate getDelegate(long nativeDrawFilter) {
+ return sManager.getDelegate(nativeDrawFilter);
+ }
+
+ public abstract boolean isSupported();
+ public abstract String getSupportMessage();
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeDestructor(long nativeDrawFilter) {
+ sManager.removeJavaReferenceFor(nativeDrawFilter);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/EmbossMaskFilter.java b/android/graphics/EmbossMaskFilter.java
new file mode 100644
index 0000000..a9e180f
--- /dev/null
+++ b/android/graphics/EmbossMaskFilter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+public class EmbossMaskFilter extends MaskFilter {
+ /**
+ * Create an emboss maskfilter
+ *
+ * @param direction array of 3 scalars [x, y, z] specifying the direction of the light source
+ * @param ambient 0...1 amount of ambient light
+ * @param specular coefficient for specular highlights (e.g. 8)
+ * @param blurRadius amount to blur before applying lighting (e.g. 3)
+ * @return the emboss maskfilter
+ */
+ public EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius) {
+ if (direction.length < 3) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ native_instance = nativeConstructor(direction, ambient, specular, blurRadius);
+ }
+
+ private static native long nativeConstructor(float[] direction, float ambient, float specular, float blurRadius);
+}
+
diff --git a/android/graphics/EmbossMaskFilter_Delegate.java b/android/graphics/EmbossMaskFilter_Delegate.java
new file mode 100644
index 0000000..e5040cc
--- /dev/null
+++ b/android/graphics/EmbossMaskFilter_Delegate.java
@@ -0,0 +1,65 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.EmbossMaskFilter
+ *
+ * Through the layoutlib_create tool, the original native methods of EmbossMaskFilter have
+ * been replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original EmbossMaskFilter class.
+ *
+ * Because this extends {@link MaskFilter_Delegate}, there's no need to use a
+ * {@link DelegateManager}, as all the Shader classes will be added to the manager
+ * owned by {@link MaskFilter_Delegate}.
+ *
+ * @see MaskFilter_Delegate
+ *
+ */
+public class EmbossMaskFilter_Delegate extends MaskFilter_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public boolean isSupported() {
+ return false;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ return "Emboss Mask Filters are not supported.";
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeConstructor(float[] direction, float ambient,
+ float specular, float blurRadius) {
+ EmbossMaskFilter_Delegate newDelegate = new EmbossMaskFilter_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/FontFamily.java b/android/graphics/FontFamily.java
new file mode 100644
index 0000000..d77e601
--- /dev/null
+++ b/android/graphics/FontFamily.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2014 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 android.graphics;
+
+import android.annotation.Nullable;
+import android.content.res.AssetManager;
+import android.graphics.fonts.FontVariationAxis;
+import android.text.FontConfig;
+import android.text.TextUtils;
+import android.util.Log;
+import dalvik.annotation.optimization.CriticalNative;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * A family of typefaces with different styles.
+ *
+ * @hide
+ */
+public class FontFamily {
+
+ private static String TAG = "FontFamily";
+
+ /**
+ * @hide
+ */
+ public long mNativePtr;
+
+ // Points native font family builder. Must be zero after freezing this family.
+ private long mBuilderPtr;
+
+ public FontFamily() {
+ mBuilderPtr = nInitBuilder(null, 0);
+ }
+
+ public FontFamily(@Nullable String[] langs, int variant) {
+ final String langsString;
+ if (langs == null || langs.length == 0) {
+ langsString = null;
+ } else if (langs.length == 1) {
+ langsString = langs[0];
+ } else {
+ langsString = TextUtils.join(",", langs);
+ }
+ mBuilderPtr = nInitBuilder(langsString, variant);
+ }
+
+ /**
+ * Finalize the FontFamily creation.
+ *
+ * @return boolean returns false if some error happens in native code, e.g. broken font file is
+ * passed, etc.
+ */
+ public boolean freeze() {
+ if (mBuilderPtr == 0) {
+ throw new IllegalStateException("This FontFamily is already frozen");
+ }
+ mNativePtr = nCreateFamily(mBuilderPtr);
+ mBuilderPtr = 0;
+ return mNativePtr != 0;
+ }
+
+ public void abortCreation() {
+ if (mBuilderPtr == 0) {
+ throw new IllegalStateException("This FontFamily is already frozen or abandoned");
+ }
+ nAbort(mBuilderPtr);
+ mBuilderPtr = 0;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mNativePtr != 0) {
+ nUnrefFamily(mNativePtr);
+ }
+ if (mBuilderPtr != 0) {
+ nAbort(mBuilderPtr);
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ public boolean addFont(String path, int ttcIndex, FontVariationAxis[] axes, int weight,
+ int italic) {
+ if (mBuilderPtr == 0) {
+ throw new IllegalStateException("Unable to call addFont after freezing.");
+ }
+ try (FileInputStream file = new FileInputStream(path)) {
+ FileChannel fileChannel = file.getChannel();
+ long fontSize = fileChannel.size();
+ ByteBuffer fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
+ if (axes != null) {
+ for (FontVariationAxis axis : axes) {
+ nAddAxisValue(mBuilderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue());
+ }
+ }
+ return nAddFont(mBuilderPtr, fontBuffer, ttcIndex, weight, italic);
+ } catch (IOException e) {
+ Log.e(TAG, "Error mapping font file " + path);
+ return false;
+ }
+ }
+
+ public boolean addFontFromBuffer(ByteBuffer font, int ttcIndex, FontVariationAxis[] axes,
+ int weight, int italic) {
+ if (mBuilderPtr == 0) {
+ throw new IllegalStateException("Unable to call addFontWeightStyle after freezing.");
+ }
+ if (axes != null) {
+ for (FontVariationAxis axis : axes) {
+ nAddAxisValue(mBuilderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue());
+ }
+ }
+ return nAddFontWeightStyle(mBuilderPtr, font, ttcIndex, weight, italic);
+ }
+
+ /**
+ * @param mgr The AssetManager to use for this context.
+ * @param path The path to the font file to load.
+ * @param cookie If available, the resource cookie given by Resources.
+ * @param isAsset {@code true} if this is from the assets/ folder, {@code false} if from
+ * resources
+ * @param weight The weight of the font. If 0 is given, the weight and italic will be resolved
+ * using the OS/2 table in the font.
+ * @param isItalic Whether this font is italic. If the weight is set to 0, this will be resolved
+ * using the OS/2 table in the font.
+ * @return
+ */
+ public boolean addFontFromAssetManager(AssetManager mgr, String path, int cookie,
+ boolean isAsset, int ttcIndex, int weight, int isItalic,
+ FontVariationAxis[] axes) {
+ if (mBuilderPtr == 0) {
+ throw new IllegalStateException("Unable to call addFontFromAsset after freezing.");
+ }
+ if (axes != null) {
+ for (FontVariationAxis axis : axes) {
+ nAddAxisValue(mBuilderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue());
+ }
+ }
+ return nAddFontFromAssetManager(mBuilderPtr, mgr, path, cookie, isAsset, ttcIndex, weight,
+ isItalic);
+ }
+
+ /**
+ * Allow creating unsupported FontFamily.
+ *
+ * For compatibility reasons, we still need to create a FontFamily object even if Minikin failed
+ * to find any usable 'cmap' table for some reasons, e.g. broken 'cmap' table, no 'cmap' table
+ * encoded with Unicode code points, etc. Without calling this method, the freeze() method will
+ * return null if Minikin fails to find any usable 'cmap' table. By calling this method, the
+ * freeze() won't fail and will create an empty FontFamily. This empty FontFamily is placed at
+ * the top of the fallback chain but is never used. if we don't create this empty FontFamily
+ * and put it at top, bad things (performance regressions, unexpected glyph selection) will
+ * happen.
+ */
+ public void allowUnsupportedFont() {
+ if (mBuilderPtr == 0) {
+ throw new IllegalStateException("Unable to allow unsupported font.");
+ }
+ nAllowUnsupportedFont(mBuilderPtr);
+ }
+
+ // TODO: Remove once internal user stop using private API.
+ private static boolean nAddFont(long builderPtr, ByteBuffer font, int ttcIndex) {
+ return nAddFont(builderPtr, font, ttcIndex, -1, -1);
+ }
+
+ private static native long nInitBuilder(String langs, int variant);
+
+ @CriticalNative
+ private static native long nCreateFamily(long mBuilderPtr);
+
+ @CriticalNative
+ private static native void nAllowUnsupportedFont(long builderPtr);
+
+ @CriticalNative
+ private static native void nAbort(long mBuilderPtr);
+
+ @CriticalNative
+ private static native void nUnrefFamily(long nativePtr);
+ // By passing -1 to weigth argument, the weight value is resolved by OS/2 table in the font.
+ // By passing -1 to italic argument, the italic value is resolved by OS/2 table in the font.
+ private static native boolean nAddFont(long builderPtr, ByteBuffer font, int ttcIndex,
+ int weight, int isItalic);
+ private static native boolean nAddFontWeightStyle(long builderPtr, ByteBuffer font,
+ int ttcIndex, int weight, int isItalic);
+ private static native boolean nAddFontFromAssetManager(long builderPtr, AssetManager mgr,
+ String path, int cookie, boolean isAsset, int ttcIndex, int weight, int isItalic);
+
+ // The added axis values are only valid for the next nAddFont* method call.
+ @CriticalNative
+ private static native void nAddAxisValue(long builderPtr, int tag, float value);
+}
diff --git a/android/graphics/FontFamily_Delegate.java b/android/graphics/FontFamily_Delegate.java
new file mode 100644
index 0000000..1e3ebd7
--- /dev/null
+++ b/android/graphics/FontFamily_Delegate.java
@@ -0,0 +1,557 @@
+/*
+ * Copyright (C) 2014 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 android.graphics;
+
+import com.android.ide.common.rendering.api.AssetRepository;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.AssetManager;
+import android.content.res.BridgeAssetManager;
+import android.graphics.fonts.FontVariationAxis;
+import android.text.FontConfig;
+
+import java.awt.Font;
+import java.awt.FontFormatException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Scanner;
+import java.util.Set;
+
+import static android.graphics.Typeface.RESOLVE_BY_FONT_TABLE;
+import static android.graphics.Typeface_Delegate.SYSTEM_FONTS;
+
+/**
+ * Delegate implementing the native methods of android.graphics.FontFamily
+ *
+ * Through the layoutlib_create tool, the original native methods of FontFamily have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original FontFamily class.
+ *
+ * @see DelegateManager
+ */
+public class FontFamily_Delegate {
+
+ public static final int DEFAULT_FONT_WEIGHT = 400;
+ public static final int BOLD_FONT_WEIGHT_DELTA = 300;
+ public static final int BOLD_FONT_WEIGHT = 700;
+
+ private static final String FONT_SUFFIX_ITALIC = "Italic.ttf";
+ private static final String FN_ALL_FONTS_LIST = "fontsInSdk.txt";
+ private static final String EXTENSION_OTF = ".otf";
+
+ private static final int CACHE_SIZE = 10;
+ // The cache has a drawback that if the font file changed after the font object was created,
+ // we will not update it.
+ private static final Map<String, FontInfo> sCache =
+ new LinkedHashMap<String, FontInfo>(CACHE_SIZE) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<String, FontInfo> eldest) {
+ return size() > CACHE_SIZE;
+ }
+
+ @Override
+ public FontInfo put(String key, FontInfo value) {
+ // renew this entry.
+ FontInfo removed = remove(key);
+ super.put(key, value);
+ return removed;
+ }
+ };
+
+ /**
+ * A class associating {@link Font} with its metadata.
+ */
+ private static final class FontInfo {
+ @Nullable
+ Font mFont;
+ int mWeight;
+ boolean mIsItalic;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ FontInfo fontInfo = (FontInfo) o;
+ return mWeight == fontInfo.mWeight && mIsItalic == fontInfo.mIsItalic;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mWeight, mIsItalic);
+ }
+
+ @Override
+ public String toString() {
+ return "FontInfo{" + "mWeight=" + mWeight + ", mIsItalic=" + mIsItalic + '}';
+ }
+ }
+
+ // ---- delegate manager ----
+ private static final DelegateManager<FontFamily_Delegate> sManager =
+ new DelegateManager<FontFamily_Delegate>(FontFamily_Delegate.class);
+
+ // ---- delegate helper data ----
+ private static String sFontLocation;
+ private static final List<FontFamily_Delegate> sPostInitDelegate = new
+ ArrayList<FontFamily_Delegate>();
+ private static Set<String> SDK_FONTS;
+
+
+ // ---- delegate data ----
+
+ // Order does not really matter but we use a LinkedHashMap to get reproducible results across
+ // render calls
+ private Map<FontInfo, Font> mFonts = new LinkedHashMap<>();
+
+ /**
+ * The variant of the Font Family - compact or elegant.
+ * <p/>
+ * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in
+ * android.graphics.FontFamily
+ *
+ * @see Paint#setElegantTextHeight(boolean)
+ */
+ private FontVariant mVariant;
+ // List of runnables to process fonts after sFontLoader is initialized.
+ private List<Runnable> mPostInitRunnables = new ArrayList<Runnable>();
+ /** @see #isValid() */
+ private boolean mValid = false;
+
+
+ // ---- Public helper class ----
+
+ public enum FontVariant {
+ // The order needs to be kept in sync with android.graphics.FontFamily.
+ NONE, COMPACT, ELEGANT
+ }
+
+ // ---- Public Helper methods ----
+
+ public static FontFamily_Delegate getDelegate(long nativeFontFamily) {
+ return sManager.getDelegate(nativeFontFamily);
+ }
+
+ public static synchronized void setFontLocation(String fontLocation) {
+ sFontLocation = fontLocation;
+ // init list of bundled fonts.
+ File allFonts = new File(fontLocation, FN_ALL_FONTS_LIST);
+ // Current number of fonts is 103. Use the next round number to leave scope for more fonts
+ // in the future.
+ Set<String> allFontsList = new HashSet<>(128);
+ Scanner scanner = null;
+ try {
+ scanner = new Scanner(allFonts);
+ while (scanner.hasNext()) {
+ String name = scanner.next();
+ // Skip font configuration files.
+ if (!name.endsWith(".xml")) {
+ allFontsList.add(name);
+ }
+ }
+ } catch (FileNotFoundException e) {
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN,
+ "Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.",
+ e, null);
+ } finally {
+ if (scanner != null) {
+ scanner.close();
+ }
+ }
+ SDK_FONTS = Collections.unmodifiableSet(allFontsList);
+ for (FontFamily_Delegate fontFamily : sPostInitDelegate) {
+ fontFamily.init();
+ }
+ sPostInitDelegate.clear();
+ }
+
+ @Nullable
+ public Font getFont(int desiredWeight, boolean isItalic) {
+ FontInfo desiredStyle = new FontInfo();
+ desiredStyle.mWeight = desiredWeight;
+ desiredStyle.mIsItalic = isItalic;
+
+ Font cachedFont = mFonts.get(desiredStyle);
+ if (cachedFont != null) {
+ return cachedFont;
+ }
+
+ FontInfo bestFont = null;
+
+ if (mFonts.size() == 1) {
+ // No need to compute the match since we only have one candidate
+ bestFont = mFonts.keySet().iterator().next();
+ } else {
+ int bestMatch = Integer.MAX_VALUE;
+
+ //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation)
+ for (FontInfo font : mFonts.keySet()) {
+ int match = computeMatch(font, desiredStyle);
+ if (match < bestMatch) {
+ bestMatch = match;
+ bestFont = font;
+ }
+ }
+
+ // This would mean that we already had the font so it should be in the set
+ assert bestMatch != 0;
+ }
+
+ if (bestFont == null) {
+ return null;
+ }
+
+
+ // Derive the font as required and add it to the list of Fonts.
+ deriveFont(bestFont, desiredStyle);
+ addFont(desiredStyle);
+ return desiredStyle.mFont;
+ }
+
+ public FontVariant getVariant() {
+ return mVariant;
+ }
+
+ /**
+ * Returns if the FontFamily should contain any fonts. If this returns true and
+ * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while
+ * loading the fonts. However, some fonts are deliberately skipped, for example they are not
+ * bundled with the SDK. In such a case, this method returns false.
+ */
+ public boolean isValid() {
+ return mValid;
+ }
+
+ private static Font loadFont(String path) {
+ if (path.startsWith(SYSTEM_FONTS) ) {
+ String relativePath = path.substring(SYSTEM_FONTS.length());
+ File f = new File(sFontLocation, relativePath);
+
+ try {
+ return Font.createFont(Font.TRUETYPE_FONT, f);
+ } catch (Exception e) {
+ if (path.endsWith(EXTENSION_OTF) && e instanceof FontFormatException) {
+ // If we aren't able to load an Open Type font, don't log a warning just yet.
+ // We wait for a case where font is being used. Only then we try to log the
+ // warning.
+ return null;
+ }
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN,
+ String.format("Unable to load font %1$s", relativePath),
+ e, null);
+ }
+ } else {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.",
+ null, null);
+ }
+
+ return null;
+ }
+
+ @Nullable
+ /*package*/ static String getFontLocation() {
+ return sFontLocation;
+ }
+
+ // ---- delegate methods ----
+ @LayoutlibDelegate
+ /*package*/ static boolean addFont(FontFamily thisFontFamily, String path, int ttcIndex,
+ FontVariationAxis[] axes, int weight, int italic) {
+ if (thisFontFamily.mBuilderPtr == 0) {
+ assert false : "Unable to call addFont after freezing.";
+ return false;
+ }
+ final FontFamily_Delegate delegate = getDelegate(thisFontFamily.mBuilderPtr);
+ return delegate != null && delegate.addFont(path, ttcIndex, weight, italic);
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nInitBuilder(String lang, int variant) {
+ // TODO: support lang. This is required for japanese locale.
+ FontFamily_Delegate delegate = new FontFamily_Delegate();
+ // variant can be 0, 1 or 2.
+ assert variant < 3;
+ delegate.mVariant = FontVariant.values()[variant];
+ if (sFontLocation != null) {
+ delegate.init();
+ } else {
+ sPostInitDelegate.add(delegate);
+ }
+ return sManager.addNewDelegate(delegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreateFamily(long builderPtr) {
+ return builderPtr;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nUnrefFamily(long nativePtr) {
+ // Removing the java reference for the object doesn't mean that it's freed for garbage
+ // collection. Typeface_Delegate may still hold a reference for it.
+ sManager.removeJavaReferenceFor(nativePtr);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nAddFont(long builderPtr, ByteBuffer font, int ttcIndex,
+ int weight, int isItalic) {
+ assert false : "The only client of this method has been overridden.";
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nAddFontWeightStyle(long builderPtr, ByteBuffer font,
+ int ttcIndex, int weight, int isItalic) {
+ assert false : "The only client of this method has been overridden.";
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAddAxisValue(long builderPtr, int tag, float value) {
+ assert false : "The only client of this method has been overridden.";
+ }
+
+ static boolean addFont(long builderPtr, final String path, final int weight,
+ final boolean isItalic) {
+ final FontFamily_Delegate delegate = getDelegate(builderPtr);
+ int italic = isItalic ? 1 : 0;
+ if (delegate != null) {
+ if (sFontLocation == null) {
+ delegate.mPostInitRunnables.add(() -> delegate.addFont(path, weight, italic));
+ return true;
+ }
+ return delegate.addFont(path, weight, italic);
+ }
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nAddFontFromAssetManager(long builderPtr, AssetManager mgr, String path,
+ int cookie, boolean isAsset, int ttcIndex, int weight, int isItalic) {
+ FontFamily_Delegate ffd = sManager.getDelegate(builderPtr);
+ if (ffd == null) {
+ return false;
+ }
+ ffd.mValid = true;
+ if (mgr == null) {
+ return false;
+ }
+ if (mgr instanceof BridgeAssetManager) {
+ InputStream fontStream = null;
+ try {
+ AssetRepository assetRepository = ((BridgeAssetManager) mgr).getAssetRepository();
+ if (assetRepository == null) {
+ Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path,
+ null);
+ return false;
+ }
+ if (!assetRepository.isSupported()) {
+ // Don't log any warnings on unsupported IDEs.
+ return false;
+ }
+ // Check cache
+ FontInfo fontInfo = sCache.get(path);
+ if (fontInfo != null) {
+ // renew the font's lease.
+ sCache.put(path, fontInfo);
+ ffd.addFont(fontInfo);
+ return true;
+ }
+ fontStream = isAsset ?
+ assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING) :
+ assetRepository.openNonAsset(cookie, path, AssetManager.ACCESS_STREAMING);
+ if (fontStream == null) {
+ Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path,
+ path);
+ return false;
+ }
+ Font font = Font.createFont(Font.TRUETYPE_FONT, fontStream);
+ fontInfo = new FontInfo();
+ fontInfo.mFont = font;
+ if (weight == RESOLVE_BY_FONT_TABLE) {
+ fontInfo.mWeight = font.isBold() ? BOLD_FONT_WEIGHT : DEFAULT_FONT_WEIGHT;
+ } else {
+ fontInfo.mWeight = weight;
+ }
+ fontInfo.mIsItalic = isItalic == RESOLVE_BY_FONT_TABLE ? font.isItalic() :
+ isItalic == 1;
+ ffd.addFont(fontInfo);
+ return true;
+ } catch (IOException e) {
+ Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Unable to load font " + path, e,
+ path);
+ } catch (FontFormatException e) {
+ if (path.endsWith(EXTENSION_OTF)) {
+ // otf fonts are not supported on the user's config (JRE version + OS)
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "OpenType fonts are not supported yet: " + path, null, path);
+ } else {
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN,
+ "Unable to load font " + path, e, path);
+ }
+ } finally {
+ if (fontStream != null) {
+ try {
+ fontStream.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ return false;
+ }
+ // This should never happen. AssetManager is a final class (from user's perspective), and
+ // we've replaced every creation of AssetManager with our implementation. We create an
+ // exception and log it, but continue with rest of the rendering, without loading this font.
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN,
+ "You have found a bug in the rendering library. Please file a bug at b.android.com.",
+ new RuntimeException("Asset Manager is not an instance of BridgeAssetManager"),
+ null);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAbort(long builderPtr) {
+ sManager.removeJavaReferenceFor(builderPtr);
+ }
+
+ /**
+ * @see FontFamily#allowUnsupportedFont
+ */
+ @LayoutlibDelegate
+ /*package*/ static void nAllowUnsupportedFont(long builderPtr) {
+ // Do nothing here as this is used for Minikin fonts
+ }
+
+ // ---- private helper methods ----
+
+ private void init() {
+ for (Runnable postInitRunnable : mPostInitRunnables) {
+ postInitRunnable.run();
+ }
+ mPostInitRunnables = null;
+ }
+
+ private boolean addFont(final String path, int ttcIndex, int weight, int italic) {
+ // FIXME: support ttc fonts. Hack JRE??
+ if (sFontLocation == null) {
+ mPostInitRunnables.add(() -> addFont(path, weight, italic));
+ return true;
+ }
+ return addFont(path, weight, italic);
+ }
+
+ private boolean addFont(@NonNull String path) {
+ return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC) ? 1 : RESOLVE_BY_FONT_TABLE);
+ }
+
+ private boolean addFont(@NonNull String path, int weight, int italic) {
+ if (path.startsWith(SYSTEM_FONTS) &&
+ !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) {
+ return mValid = false;
+ }
+ // Set valid to true, even if the font fails to load.
+ mValid = true;
+ Font font = loadFont(path);
+ if (font == null) {
+ return false;
+ }
+ FontInfo fontInfo = new FontInfo();
+ fontInfo.mFont = font;
+ fontInfo.mWeight = weight;
+ fontInfo.mIsItalic = italic == RESOLVE_BY_FONT_TABLE ? font.isItalic() : italic == 1;
+ addFont(fontInfo);
+ return true;
+ }
+
+ private boolean addFont(@NonNull FontInfo fontInfo) {
+ return mFonts.putIfAbsent(fontInfo, fontInfo.mFont) == null;
+ }
+
+ /**
+ * Compute matching metric between two styles - 0 is an exact match.
+ */
+ private static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) {
+ int score = Math.abs(font1.mWeight - font2.mWeight);
+ if (font1.mIsItalic != font2.mIsItalic) {
+ score += 200;
+ }
+ return score;
+ }
+
+ /**
+ * Try to derive a font from {@code srcFont} for the style in {@code outFont}.
+ * <p/>
+ * {@code outFont} is updated to reflect the style of the derived font.
+ * @param srcFont the source font
+ * @param outFont contains the desired font style. Updated to contain the derived font and
+ * its style
+ * @return outFont
+ */
+ private void deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) {
+ int desiredWeight = outFont.mWeight;
+ int srcWeight = srcFont.mWeight;
+ assert srcFont.mFont != null;
+ Font derivedFont = srcFont.mFont;
+ int derivedStyle = 0;
+ // Embolden the font if required.
+ if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) {
+ derivedStyle |= Font.BOLD;
+ srcWeight += BOLD_FONT_WEIGHT_DELTA;
+ }
+ // Italicize the font if required.
+ if (outFont.mIsItalic && !srcFont.mIsItalic) {
+ derivedStyle |= Font.ITALIC;
+ } else if (outFont.mIsItalic != srcFont.mIsItalic) {
+ // The desired font is plain, but the src font is italics. We can't convert it back. So
+ // we update the value to reflect the true style of the font we're deriving.
+ outFont.mIsItalic = srcFont.mIsItalic;
+ }
+
+ if (derivedStyle != 0) {
+ derivedFont = derivedFont.deriveFont(derivedStyle);
+ }
+
+ outFont.mFont = derivedFont;
+ outFont.mWeight = srcWeight;
+ // No need to update mIsItalics, as it's already been handled above.
+ }
+}
diff --git a/android/graphics/FontListParser.java b/android/graphics/FontListParser.java
new file mode 100644
index 0000000..9f672e3
--- /dev/null
+++ b/android/graphics/FontListParser.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2014 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 android.graphics;
+
+import android.text.FontConfig;
+import android.graphics.fonts.FontVariationAxis;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for font config files.
+ *
+ * @hide
+ */
+public class FontListParser {
+
+ /* Parse fallback list (no names) */
+ public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException {
+ try {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(in, null);
+ parser.nextTag();
+ return readFamilies(parser);
+ } finally {
+ in.close();
+ }
+ }
+
+ private static FontConfig readFamilies(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ List<FontConfig.Family> families = new ArrayList<>();
+ List<FontConfig.Alias> aliases = new ArrayList<>();
+
+ parser.require(XmlPullParser.START_TAG, null, "familyset");
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) continue;
+ String tag = parser.getName();
+ if (tag.equals("family")) {
+ families.add(readFamily(parser));
+ } else if (tag.equals("alias")) {
+ aliases.add(readAlias(parser));
+ } else {
+ skip(parser);
+ }
+ }
+ return new FontConfig(families.toArray(new FontConfig.Family[families.size()]),
+ aliases.toArray(new FontConfig.Alias[aliases.size()]));
+ }
+
+ private static FontConfig.Family readFamily(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ final String name = parser.getAttributeValue(null, "name");
+ final String lang = parser.getAttributeValue(null, "lang");
+ final String[] langs = lang == null ? null : lang.split("\\s+");
+ final String variant = parser.getAttributeValue(null, "variant");
+ final List<FontConfig.Font> fonts = new ArrayList<FontConfig.Font>();
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) continue;
+ final String tag = parser.getName();
+ if (tag.equals("font")) {
+ fonts.add(readFont(parser));
+ } else {
+ skip(parser);
+ }
+ }
+ int intVariant = FontConfig.Family.VARIANT_DEFAULT;
+ if (variant != null) {
+ if (variant.equals("compact")) {
+ intVariant = FontConfig.Family.VARIANT_COMPACT;
+ } else if (variant.equals("elegant")) {
+ intVariant = FontConfig.Family.VARIANT_ELEGANT;
+ }
+ }
+ return new FontConfig.Family(name, fonts.toArray(new FontConfig.Font[fonts.size()]), langs,
+ intVariant);
+ }
+
+ /** Matches leading and trailing XML whitespace. */
+ private static final Pattern FILENAME_WHITESPACE_PATTERN =
+ Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
+
+ private static FontConfig.Font readFont(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ String indexStr = parser.getAttributeValue(null, "index");
+ int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
+ List<FontVariationAxis> axes = new ArrayList<FontVariationAxis>();
+ String weightStr = parser.getAttributeValue(null, "weight");
+ int weight = weightStr == null ? 400 : Integer.parseInt(weightStr);
+ boolean isItalic = "italic".equals(parser.getAttributeValue(null, "style"));
+ String fallbackFor = parser.getAttributeValue(null, "fallbackFor");
+ StringBuilder filename = new StringBuilder();
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() == XmlPullParser.TEXT) {
+ filename.append(parser.getText());
+ }
+ if (parser.getEventType() != XmlPullParser.START_TAG) continue;
+ String tag = parser.getName();
+ if (tag.equals("axis")) {
+ axes.add(readAxis(parser));
+ } else {
+ skip(parser);
+ }
+ }
+ String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
+ return new FontConfig.Font(sanitizedName, index,
+ axes.toArray(new FontVariationAxis[axes.size()]), weight, isItalic, fallbackFor);
+ }
+
+ private static FontVariationAxis readAxis(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ String tagStr = parser.getAttributeValue(null, "tag");
+ String styleValueStr = parser.getAttributeValue(null, "stylevalue");
+ skip(parser); // axis tag is empty, ignore any contents and consume end tag
+ return new FontVariationAxis(tagStr, Float.parseFloat(styleValueStr));
+ }
+
+ private static FontConfig.Alias readAlias(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ String name = parser.getAttributeValue(null, "name");
+ String toName = parser.getAttributeValue(null, "to");
+ String weightStr = parser.getAttributeValue(null, "weight");
+ int weight;
+ if (weightStr == null) {
+ weight = 400;
+ } else {
+ weight = Integer.parseInt(weightStr);
+ }
+ skip(parser); // alias tag is empty, ignore any contents and consume end tag
+ return new FontConfig.Alias(name, toName, weight);
+ }
+
+ private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
+ int depth = 1;
+ while (depth > 0) {
+ switch (parser.next()) {
+ case XmlPullParser.START_TAG:
+ depth++;
+ break;
+ case XmlPullParser.END_TAG:
+ depth--;
+ break;
+ }
+ }
+ }
+}
diff --git a/android/graphics/Gradient_Delegate.java b/android/graphics/Gradient_Delegate.java
new file mode 100644
index 0000000..64410e4
--- /dev/null
+++ b/android/graphics/Gradient_Delegate.java
@@ -0,0 +1,212 @@
+/*
+ * 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 android.graphics;
+
+import android.graphics.Shader.TileMode;
+
+/**
+ * Base class for true Gradient shader delegate.
+ */
+public abstract class Gradient_Delegate extends Shader_Delegate {
+
+ protected final int[] mColors;
+ protected final float[] mPositions;
+
+ @Override
+ public boolean isSupported() {
+ // all gradient shaders are supported.
+ return true;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ // all gradient shaders are supported, no need for a gradient support
+ return null;
+ }
+
+ /**
+ * Creates the base shader and do some basic test on the parameters.
+ *
+ * @param nativeMatrix reference to the shader's native transformation matrix
+ * @param colors The colors to be distributed along the gradient line
+ * @param positions May be null. The relative positions [0..1] of each
+ * corresponding color in the colors array. If this is null, the
+ * the colors are distributed evenly along the gradient line.
+ */
+ protected Gradient_Delegate(long nativeMatrix, int colors[], float positions[]) {
+ super(nativeMatrix);
+ assert colors.length >= 2 : "needs >= 2 number of colors";
+
+ if (positions == null) {
+ float spacing = 1.f / (colors.length - 1);
+ positions = new float[colors.length];
+ positions[0] = 0.f;
+ positions[colors.length - 1] = 1.f;
+ for (int i = 1; i < colors.length - 1; i++) {
+ positions[i] = spacing * i;
+ }
+ } else {
+ assert colors.length == positions.length :
+ "color and position " + "arrays must be of equal length";
+ }
+
+ mColors = colors;
+ mPositions = positions;
+ }
+
+ /**
+ * Base class for (Java) Gradient Paints. This handles computing the gradient colors based
+ * on the color and position lists, as well as the {@link TileMode}
+ *
+ */
+ protected abstract static class GradientPaint implements java.awt.Paint {
+ private final static int GRADIENT_SIZE = 100;
+
+ private final int[] mColors;
+ private final float[] mPositions;
+ private final TileMode mTileMode;
+ private int[] mGradient;
+
+ protected GradientPaint(int[] colors, float[] positions, TileMode tileMode) {
+ mColors = colors;
+ mPositions = positions;
+ mTileMode = tileMode;
+ }
+
+ @Override
+ public int getTransparency() {
+ return java.awt.Paint.TRANSLUCENT;
+ }
+
+ /**
+ * Pre-computes the colors for the gradient. This must be called once before any call
+ * to {@link #getGradientColor(float)}
+ */
+ protected void precomputeGradientColors() {
+ 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] = computeColor(mColors[prevPos], mColors[nextPos], percent);
+ }
+ }
+ }
+
+ /**
+ * Returns the color based on the position in the gradient.
+ * <var>pos</var> can be anything, even < 0 or > > 1, as the gradient
+ * will use {@link TileMode} value to convert it into a [0,1] value.
+ */
+ protected int getGradientColor(float pos) {
+ if (pos < 0.f) {
+ if (mTileMode != null) {
+ switch (mTileMode) {
+ case CLAMP:
+ pos = 0.f;
+ break;
+ case REPEAT:
+ // remove the integer part to stay in the [0,1] range.
+ // we also need to invert the value from [-1,0] to [0, 1]
+ pos = pos - (float)Math.floor(pos);
+ break;
+ case MIRROR:
+ // this is the same as the positive side, just make the value positive
+ // first.
+ pos = Math.abs(pos);
+
+ // 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;
+ }
+ } else {
+ pos = 0.0f;
+ }
+ } else if (pos > 1f) {
+ if (mTileMode != null) {
+ switch (mTileMode) {
+ 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;
+ }
+ } else {
+ pos = 1.0f;
+ }
+ }
+
+ int index = (int)((pos * GRADIENT_SIZE) + .5);
+
+ return mGradient[index];
+ }
+
+ /**
+ * Returns the color between c1, and c2, based on the percent of the distance
+ * between c1 and c2.
+ */
+ private int computeColor(int c1, int c2, float percent) {
+ int a = computeChannel((c1 >> 24) & 0xFF, (c2 >> 24) & 0xFF, percent);
+ int r = computeChannel((c1 >> 16) & 0xFF, (c2 >> 16) & 0xFF, percent);
+ int g = computeChannel((c1 >> 8) & 0xFF, (c2 >> 8) & 0xFF, percent);
+ int b = computeChannel((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 computeChannel(int c1, int c2, float percent) {
+ return c1 + (int)((percent * (c2-c1)) + .5);
+ }
+ }
+}
diff --git a/android/graphics/GraphicBuffer.java b/android/graphics/GraphicBuffer.java
new file mode 100644
index 0000000..53d2177
--- /dev/null
+++ b/android/graphics/GraphicBuffer.java
@@ -0,0 +1,303 @@
+/*
+ * 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 android.graphics;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Simple wrapper for the native GraphicBuffer class.
+ *
+ * @hide
+ */
+@SuppressWarnings("UnusedDeclaration")
+public class GraphicBuffer implements Parcelable {
+ // Note: keep usage flags in sync with GraphicBuffer.h and gralloc.h
+ public static final int USAGE_SW_READ_NEVER = 0x0;
+ public static final int USAGE_SW_READ_RARELY = 0x2;
+ public static final int USAGE_SW_READ_OFTEN = 0x3;
+ public static final int USAGE_SW_READ_MASK = 0xF;
+
+ public static final int USAGE_SW_WRITE_NEVER = 0x0;
+ public static final int USAGE_SW_WRITE_RARELY = 0x20;
+ public static final int USAGE_SW_WRITE_OFTEN = 0x30;
+ public static final int USAGE_SW_WRITE_MASK = 0xF0;
+
+ public static final int USAGE_SOFTWARE_MASK = USAGE_SW_READ_MASK | USAGE_SW_WRITE_MASK;
+
+ public static final int USAGE_PROTECTED = 0x4000;
+
+ public static final int USAGE_HW_TEXTURE = 0x100;
+ public static final int USAGE_HW_RENDER = 0x200;
+ public static final int USAGE_HW_2D = 0x400;
+ public static final int USAGE_HW_COMPOSER = 0x800;
+ public static final int USAGE_HW_VIDEO_ENCODER = 0x10000;
+ public static final int USAGE_HW_MASK = 0x71F00;
+
+ private final int mWidth;
+ private final int mHeight;
+ private final int mFormat;
+ private final int mUsage;
+ // Note: do not rename, this field is used by native code
+ private final long mNativeObject;
+
+ // These two fields are only used by lock/unlockCanvas()
+ private Canvas mCanvas;
+ private int mSaveCount;
+
+ // If set to true, this GraphicBuffer instance cannot be used anymore
+ private boolean mDestroyed;
+
+ /**
+ * Creates new <code>GraphicBuffer</code> instance. This method will return null
+ * if the buffer cannot be created.
+ *
+ * @param width The width in pixels of the buffer
+ * @param height The height in pixels of the buffer
+ * @param format The format of each pixel as specified in {@link PixelFormat}
+ * @param usage Hint indicating how the buffer will be used
+ *
+ * @return A <code>GraphicBuffer</code> instance or null
+ */
+ public static GraphicBuffer create(int width, int height, int format, int usage) {
+ long nativeObject = nCreateGraphicBuffer(width, height, format, usage);
+ if (nativeObject != 0) {
+ return new GraphicBuffer(width, height, format, usage, nativeObject);
+ }
+ return null;
+ }
+
+ /**
+ * Private use only. See {@link #create(int, int, int, int)}.
+ */
+ private GraphicBuffer(int width, int height, int format, int usage, long nativeObject) {
+ mWidth = width;
+ mHeight = height;
+ mFormat = format;
+ mUsage = usage;
+ mNativeObject = nativeObject;
+ }
+
+ /**
+ * For SurfaceControl JNI.
+ * @hide
+ */
+ public static GraphicBuffer createFromExisting(int width, int height,
+ int format, int usage, long unwrappedNativeObject) {
+ long nativeObject = nWrapGraphicBuffer(unwrappedNativeObject);
+ if (nativeObject != 0) {
+ return new GraphicBuffer(width, height, format, usage, nativeObject);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the width of this buffer in pixels.
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Returns the height of this buffer in pixels.
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Returns the pixel format of this buffer. The pixel format must be one of
+ * the formats defined in {@link PixelFormat}.
+ */
+ public int getFormat() {
+ return mFormat;
+ }
+
+ /**
+ * Returns the usage hint set on this buffer.
+ */
+ public int getUsage() {
+ return mUsage;
+ }
+
+ /**
+ * <p>Start editing the pixels in the buffer. A null is returned if the buffer
+ * cannot be locked for editing.</p>
+ *
+ * <p>The content of the buffer is preserved between unlockCanvas()
+ * and lockCanvas().</p>
+ *
+ * <p>If this method is called after {@link #destroy()}, the return value will
+ * always be null.</p>
+ *
+ * @return A Canvas used to draw into the buffer, or null.
+ *
+ * @see #lockCanvas(android.graphics.Rect)
+ * @see #unlockCanvasAndPost(android.graphics.Canvas)
+ * @see #isDestroyed()
+ */
+ public Canvas lockCanvas() {
+ return lockCanvas(null);
+ }
+
+ /**
+ * Just like {@link #lockCanvas()} but allows specification of a dirty
+ * rectangle.
+ *
+ * <p>If this method is called after {@link #destroy()}, the return value will
+ * always be null.</p>
+ *
+ * @param dirty Area of the buffer that may be modified.
+
+ * @return A Canvas used to draw into the surface, or null.
+ *
+ * @see #lockCanvas()
+ * @see #unlockCanvasAndPost(android.graphics.Canvas)
+ * @see #isDestroyed()
+ */
+ public Canvas lockCanvas(Rect dirty) {
+ if (mDestroyed) {
+ return null;
+ }
+
+ if (mCanvas == null) {
+ mCanvas = new Canvas();
+ }
+
+ if (nLockCanvas(mNativeObject, mCanvas, dirty)) {
+ mSaveCount = mCanvas.save();
+ return mCanvas;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finish editing pixels in the buffer.
+ *
+ * <p>This method doesn't do anything if {@link #destroy()} was
+ * previously called.</p>
+ *
+ * @param canvas The Canvas previously returned by lockCanvas()
+ *
+ * @see #lockCanvas()
+ * @see #lockCanvas(android.graphics.Rect)
+ * @see #isDestroyed()
+ */
+ public void unlockCanvasAndPost(Canvas canvas) {
+ if (!mDestroyed && mCanvas != null && canvas == mCanvas) {
+ canvas.restoreToCount(mSaveCount);
+ mSaveCount = 0;
+
+ nUnlockCanvasAndPost(mNativeObject, mCanvas);
+ }
+ }
+
+ /**
+ * Destroyes this buffer immediately. Calling this method frees up any
+ * underlying native resources. After calling this method, this buffer
+ * must not be used in any way ({@link #lockCanvas()} must not be called,
+ * etc.)
+ *
+ * @see #isDestroyed()
+ */
+ public void destroy() {
+ if (!mDestroyed) {
+ mDestroyed = true;
+ nDestroyGraphicBuffer(mNativeObject);
+ }
+ }
+
+ /**
+ * Indicates whether this buffer has been destroyed. A destroyed buffer
+ * cannot be used in any way: locking a Canvas will return null, the buffer
+ * cannot be written to a parcel, etc.
+ *
+ * @return True if this <code>GraphicBuffer</code> is in a destroyed state,
+ * false otherwise.
+ *
+ * @see #destroy()
+ */
+ public boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (!mDestroyed) nDestroyGraphicBuffer(mNativeObject);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Flatten this object in to a Parcel.
+ *
+ * <p>Calling this method will throw an <code>IllegalStateException</code> if
+ * {@link #destroy()} has been previously called.</p>
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written.
+ * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mDestroyed) {
+ throw new IllegalStateException("This GraphicBuffer has been destroyed and cannot be "
+ + "written to a parcel.");
+ }
+
+ dest.writeInt(mWidth);
+ dest.writeInt(mHeight);
+ dest.writeInt(mFormat);
+ dest.writeInt(mUsage);
+ nWriteGraphicBufferToParcel(mNativeObject, dest);
+ }
+
+ public static final Parcelable.Creator<GraphicBuffer> CREATOR =
+ new Parcelable.Creator<GraphicBuffer>() {
+ public GraphicBuffer createFromParcel(Parcel in) {
+ int width = in.readInt();
+ int height = in.readInt();
+ int format = in.readInt();
+ int usage = in.readInt();
+ long nativeObject = nReadGraphicBufferFromParcel(in);
+ if (nativeObject != 0) {
+ return new GraphicBuffer(width, height, format, usage, nativeObject);
+ }
+ return null;
+ }
+
+ public GraphicBuffer[] newArray(int size) {
+ return new GraphicBuffer[size];
+ }
+ };
+
+ private static native long nCreateGraphicBuffer(int width, int height, int format, int usage);
+ private static native void nDestroyGraphicBuffer(long nativeObject);
+ private static native void nWriteGraphicBufferToParcel(long nativeObject, Parcel dest);
+ private static native long nReadGraphicBufferFromParcel(Parcel in);
+ private static native boolean nLockCanvas(long nativeObject, Canvas canvas, Rect dirty);
+ private static native boolean nUnlockCanvasAndPost(long nativeObject, Canvas canvas);
+ private static native long nWrapGraphicBuffer(long nativeObject);
+}
diff --git a/android/graphics/ImageFormat.java b/android/graphics/ImageFormat.java
new file mode 100644
index 0000000..e3527e3
--- /dev/null
+++ b/android/graphics/ImageFormat.java
@@ -0,0 +1,784 @@
+/*
+ * 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 android.graphics;
+
+public class ImageFormat {
+ /*
+ * these constants are chosen to be binary compatible with their previous
+ * location in PixelFormat.java
+ */
+
+ public static final int UNKNOWN = 0;
+
+ /**
+ * RGB format used for pictures encoded as RGB_565. See
+ * {@link android.hardware.Camera.Parameters#setPictureFormat(int)}.
+ */
+ public static final int RGB_565 = 4;
+
+ /**
+ * <p>Android YUV format.</p>
+ *
+ * <p>This format is exposed to software decoders and applications.</p>
+ *
+ * <p>YV12 is a 4:2:0 YCrCb planar format comprised of a WxH Y plane followed
+ * by (W/2) x (H/2) Cr and Cb planes.</p>
+ *
+ * <p>This format assumes
+ * <ul>
+ * <li>an even width</li>
+ * <li>an even height</li>
+ * <li>a horizontal stride multiple of 16 pixels</li>
+ * <li>a vertical stride equal to the height</li>
+ * </ul>
+ * </p>
+ *
+ * <pre> y_size = stride * height
+ * c_stride = ALIGN(stride/2, 16)
+ * c_size = c_stride * height/2
+ * size = y_size + c_size * 2
+ * cr_offset = y_size
+ * cb_offset = y_size + c_size</pre>
+ *
+ * <p>For the {@link android.hardware.camera2} API, the {@link #YUV_420_888} format is
+ * recommended for YUV output instead.</p>
+ *
+ * <p>For the older camera API, this format is guaranteed to be supported for
+ * {@link android.hardware.Camera} preview images since API level 12; for earlier API versions,
+ * check {@link android.hardware.Camera.Parameters#getSupportedPreviewFormats()}.
+ *
+ * <p>Note that for camera preview callback use (see
+ * {@link android.hardware.Camera#setPreviewCallback}), the
+ * <var>stride</var> value is the smallest possible; that is, it is equal
+ * to:
+ *
+ * <pre>stride = ALIGN(width, 16)</pre>
+ *
+ * @see android.hardware.Camera.Parameters#setPreviewCallback
+ * @see android.hardware.Camera.Parameters#setPreviewFormat
+ * @see android.hardware.Camera.Parameters#getSupportedPreviewFormats
+ * </p>
+ */
+ public static final int YV12 = 0x32315659;
+
+ /**
+ * <p>Android Y8 format.</p>
+ *
+ * <p>Y8 is a YUV planar format comprised of a WxH Y plane only, with each pixel
+ * being represented by 8 bits. It is equivalent to just the Y plane from {@link #YV12}
+ * format.</p>
+ *
+ * <p>This format assumes
+ * <ul>
+ * <li>an even width</li>
+ * <li>an even height</li>
+ * <li>a horizontal stride multiple of 16 pixels</li>
+ * </ul>
+ * </p>
+ *
+ * <pre> y_size = stride * height </pre>
+ *
+ * <p>For example, the {@link android.media.Image} object can provide data
+ * in this format from a {@link android.hardware.camera2.CameraDevice}
+ * through a {@link android.media.ImageReader} object if this format is
+ * supported by {@link android.hardware.camera2.CameraDevice}.</p>
+ *
+ * @see android.media.Image
+ * @see android.media.ImageReader
+ * @see android.hardware.camera2.CameraDevice
+ *
+ * @hide
+ */
+ public static final int Y8 = 0x20203859;
+
+ /**
+ * <p>Android Y16 format.</p>
+ *
+ * Y16 is a YUV planar format comprised of a WxH Y plane, with each pixel
+ * being represented by 16 bits. It is just like {@link #Y8}, but has 16
+ * bits per pixel (little endian).</p>
+ *
+ * <p>This format assumes
+ * <ul>
+ * <li>an even width</li>
+ * <li>an even height</li>
+ * <li>a horizontal stride multiple of 16 pixels</li>
+ * </ul>
+ * </p>
+ *
+ * <pre> y_size = stride * height </pre>
+ *
+ * <p>For example, the {@link android.media.Image} object can provide data
+ * in this format from a {@link android.hardware.camera2.CameraDevice}
+ * through a {@link android.media.ImageReader} object if this format is
+ * supported by {@link android.hardware.camera2.CameraDevice}.</p>
+ *
+ * @see android.media.Image
+ * @see android.media.ImageReader
+ * @see android.hardware.camera2.CameraDevice
+ *
+ * @hide
+ */
+ public static final int Y16 = 0x20363159;
+
+ /**
+ * YCbCr format, used for video.
+ *
+ * <p>For the {@link android.hardware.camera2} API, the {@link #YUV_420_888} format is
+ * recommended for YUV output instead.</p>
+ *
+ * <p>Whether this format is supported by the old camera API can be determined by
+ * {@link android.hardware.Camera.Parameters#getSupportedPreviewFormats()}.</p>
+ *
+ */
+ public static final int NV16 = 0x10;
+
+ /**
+ * YCrCb format used for images, which uses the NV21 encoding format.
+ *
+ * <p>This is the default format
+ * for {@link android.hardware.Camera} preview images, when not otherwise set with
+ * {@link android.hardware.Camera.Parameters#setPreviewFormat(int)}.</p>
+ *
+ * <p>For the {@link android.hardware.camera2} API, the {@link #YUV_420_888} format is
+ * recommended for YUV output instead.</p>
+ */
+ public static final int NV21 = 0x11;
+
+ /**
+ * YCbCr format used for images, which uses YUYV (YUY2) encoding format.
+ *
+ * <p>For the {@link android.hardware.camera2} API, the {@link #YUV_420_888} format is
+ * recommended for YUV output instead.</p>
+ *
+ * <p>This is an alternative format for {@link android.hardware.Camera} preview images. Whether
+ * this format is supported by the camera hardware can be determined by
+ * {@link android.hardware.Camera.Parameters#getSupportedPreviewFormats()}.</p>
+ */
+ public static final int YUY2 = 0x14;
+
+ /**
+ * Compressed JPEG format.
+ *
+ * <p>This format is always supported as an output format for the
+ * {@link android.hardware.camera2} API, and as a picture format for the older
+ * {@link android.hardware.Camera} API</p>
+ */
+ public static final int JPEG = 0x100;
+
+ /**
+ * <p>Multi-plane Android YUV 420 format</p>
+ *
+ * <p>This format is a generic YCbCr format, capable of describing any 4:2:0
+ * chroma-subsampled planar or semiplanar buffer (but not fully interleaved),
+ * with 8 bits per color sample.</p>
+ *
+ * <p>Images in this format are always represented by three separate buffers
+ * of data, one for each color plane. Additional information always
+ * accompanies the buffers, describing the row stride and the pixel stride
+ * for each plane.</p>
+ *
+ * <p>The order of planes in the array returned by
+ * {@link android.media.Image#getPlanes() Image#getPlanes()} is guaranteed such that
+ * plane #0 is always Y, plane #1 is always U (Cb), and plane #2 is always V (Cr).</p>
+ *
+ * <p>The Y-plane is guaranteed not to be interleaved with the U/V planes
+ * (in particular, pixel stride is always 1 in
+ * {@link android.media.Image.Plane#getPixelStride() yPlane.getPixelStride()}).</p>
+ *
+ * <p>The U/V planes are guaranteed to have the same row stride and pixel stride
+ * (in particular,
+ * {@link android.media.Image.Plane#getRowStride() uPlane.getRowStride()}
+ * == {@link android.media.Image.Plane#getRowStride() vPlane.getRowStride()} and
+ * {@link android.media.Image.Plane#getPixelStride() uPlane.getPixelStride()}
+ * == {@link android.media.Image.Plane#getPixelStride() vPlane.getPixelStride()};
+ * ).</p>
+ *
+ * <p>For example, the {@link android.media.Image} object can provide data
+ * in this format from a {@link android.hardware.camera2.CameraDevice}
+ * through a {@link android.media.ImageReader} object.</p>
+ *
+ * @see android.media.Image
+ * @see android.media.ImageReader
+ * @see android.hardware.camera2.CameraDevice
+ */
+ public static final int YUV_420_888 = 0x23;
+
+ /**
+ * <p>Multi-plane Android YUV 422 format</p>
+ *
+ * <p>This format is a generic YCbCr format, capable of describing any 4:2:2
+ * chroma-subsampled (planar, semiplanar or interleaved) format,
+ * with 8 bits per color sample.</p>
+ *
+ * <p>Images in this format are always represented by three separate buffers
+ * of data, one for each color plane. Additional information always
+ * accompanies the buffers, describing the row stride and the pixel stride
+ * for each plane.</p>
+ *
+ * <p>The order of planes in the array returned by
+ * {@link android.media.Image#getPlanes() Image#getPlanes()} is guaranteed such that
+ * plane #0 is always Y, plane #1 is always U (Cb), and plane #2 is always V (Cr).</p>
+ *
+ * <p>In contrast to the {@link #YUV_420_888} format, the Y-plane may have a pixel
+ * stride greater than 1 in
+ * {@link android.media.Image.Plane#getPixelStride() yPlane.getPixelStride()}.</p>
+ *
+ * <p>The U/V planes are guaranteed to have the same row stride and pixel stride
+ * (in particular,
+ * {@link android.media.Image.Plane#getRowStride() uPlane.getRowStride()}
+ * == {@link android.media.Image.Plane#getRowStride() vPlane.getRowStride()} and
+ * {@link android.media.Image.Plane#getPixelStride() uPlane.getPixelStride()}
+ * == {@link android.media.Image.Plane#getPixelStride() vPlane.getPixelStride()};
+ * ).</p>
+ *
+ * <p>For example, the {@link android.media.Image} object can provide data
+ * in this format from a {@link android.media.MediaCodec}
+ * through {@link android.media.MediaCodec#getOutputImage} object.</p>
+ *
+ * @see android.media.Image
+ * @see android.media.MediaCodec
+ */
+ public static final int YUV_422_888 = 0x27;
+
+ /**
+ * <p>Multi-plane Android YUV 444 format</p>
+ *
+ * <p>This format is a generic YCbCr format, capable of describing any 4:4:4
+ * (planar, semiplanar or interleaved) format,
+ * with 8 bits per color sample.</p>
+ *
+ * <p>Images in this format are always represented by three separate buffers
+ * of data, one for each color plane. Additional information always
+ * accompanies the buffers, describing the row stride and the pixel stride
+ * for each plane.</p>
+ *
+ * <p>The order of planes in the array returned by
+ * {@link android.media.Image#getPlanes() Image#getPlanes()} is guaranteed such that
+ * plane #0 is always Y, plane #1 is always U (Cb), and plane #2 is always V (Cr).</p>
+ *
+ * <p>In contrast to the {@link #YUV_420_888} format, the Y-plane may have a pixel
+ * stride greater than 1 in
+ * {@link android.media.Image.Plane#getPixelStride() yPlane.getPixelStride()}.</p>
+ *
+ * <p>The U/V planes are guaranteed to have the same row stride and pixel stride
+ * (in particular,
+ * {@link android.media.Image.Plane#getRowStride() uPlane.getRowStride()}
+ * == {@link android.media.Image.Plane#getRowStride() vPlane.getRowStride()} and
+ * {@link android.media.Image.Plane#getPixelStride() uPlane.getPixelStride()}
+ * == {@link android.media.Image.Plane#getPixelStride() vPlane.getPixelStride()};
+ * ).</p>
+ *
+ * <p>For example, the {@link android.media.Image} object can provide data
+ * in this format from a {@link android.media.MediaCodec}
+ * through {@link android.media.MediaCodec#getOutputImage} object.</p>
+ *
+ * @see android.media.Image
+ * @see android.media.MediaCodec
+ */
+ public static final int YUV_444_888 = 0x28;
+
+ /**
+ * <p>Multi-plane Android RGB format</p>
+ *
+ * <p>This format is a generic RGB format, capable of describing most RGB formats,
+ * with 8 bits per color sample.</p>
+ *
+ * <p>Images in this format are always represented by three separate buffers
+ * of data, one for each color plane. Additional information always
+ * accompanies the buffers, describing the row stride and the pixel stride
+ * for each plane.</p>
+ *
+ * <p>The order of planes in the array returned by
+ * {@link android.media.Image#getPlanes() Image#getPlanes()} is guaranteed such that
+ * plane #0 is always R (red), plane #1 is always G (green), and plane #2 is always B
+ * (blue).</p>
+ *
+ * <p>All three planes are guaranteed to have the same row strides and pixel strides.</p>
+ *
+ * <p>For example, the {@link android.media.Image} object can provide data
+ * in this format from a {@link android.media.MediaCodec}
+ * through {@link android.media.MediaCodec#getOutputImage} object.</p>
+ *
+ * @see android.media.Image
+ * @see android.media.MediaCodec
+ */
+ public static final int FLEX_RGB_888 = 0x29;
+
+ /**
+ * <p>Multi-plane Android RGBA format</p>
+ *
+ * <p>This format is a generic RGBA format, capable of describing most RGBA formats,
+ * with 8 bits per color sample.</p>
+ *
+ * <p>Images in this format are always represented by four separate buffers
+ * of data, one for each color plane. Additional information always
+ * accompanies the buffers, describing the row stride and the pixel stride
+ * for each plane.</p>
+ *
+ * <p>The order of planes in the array returned by
+ * {@link android.media.Image#getPlanes() Image#getPlanes()} is guaranteed such that
+ * plane #0 is always R (red), plane #1 is always G (green), plane #2 is always B (blue),
+ * and plane #3 is always A (alpha). This format may represent pre-multiplied or
+ * non-premultiplied alpha.</p>
+ *
+ * <p>All four planes are guaranteed to have the same row strides and pixel strides.</p>
+ *
+ * <p>For example, the {@link android.media.Image} object can provide data
+ * in this format from a {@link android.media.MediaCodec}
+ * through {@link android.media.MediaCodec#getOutputImage} object.</p>
+ *
+ * @see android.media.Image
+ * @see android.media.MediaCodec
+ */
+ public static final int FLEX_RGBA_8888 = 0x2A;
+
+ /**
+ * <p>General raw camera sensor image format, usually representing a
+ * single-channel Bayer-mosaic image. Each pixel color sample is stored with
+ * 16 bits of precision.</p>
+ *
+ * <p>The layout of the color mosaic, the maximum and minimum encoding
+ * values of the raw pixel data, the color space of the image, and all other
+ * needed information to interpret a raw sensor image must be queried from
+ * the {@link android.hardware.camera2.CameraDevice} which produced the
+ * image.</p>
+ */
+ public static final int RAW_SENSOR = 0x20;
+
+ /**
+ * <p>Private raw camera sensor image format, a single channel image with
+ * implementation depedent pixel layout.</p>
+ *
+ * <p>RAW_PRIVATE is a format for unprocessed raw image buffers coming from an
+ * image sensor. The actual structure of buffers of this format is
+ * implementation-dependent.</p>
+ *
+ */
+ public static final int RAW_PRIVATE = 0x24;
+
+ /**
+ * <p>
+ * Android 10-bit raw format
+ * </p>
+ * <p>
+ * This is a single-plane, 10-bit per pixel, densely packed (in each row),
+ * unprocessed format, usually representing raw Bayer-pattern images coming
+ * from an image sensor.
+ * </p>
+ * <p>
+ * In an image buffer with this format, starting from the first pixel of
+ * each row, each 4 consecutive pixels are packed into 5 bytes (40 bits).
+ * Each one of the first 4 bytes contains the top 8 bits of each pixel, The
+ * fifth byte contains the 2 least significant bits of the 4 pixels, the
+ * exact layout data for each 4 consecutive pixels is illustrated below
+ * ({@code Pi[j]} stands for the jth bit of the ith pixel):
+ * </p>
+ * <table>
+ * <thead>
+ * <tr>
+ * <th align="center"></th>
+ * <th align="center">bit 7</th>
+ * <th align="center">bit 6</th>
+ * <th align="center">bit 5</th>
+ * <th align="center">bit 4</th>
+ * <th align="center">bit 3</th>
+ * <th align="center">bit 2</th>
+ * <th align="center">bit 1</th>
+ * <th align="center">bit 0</th>
+ * </tr>
+ * </thead> <tbody>
+ * <tr>
+ * <td align="center">Byte 0:</td>
+ * <td align="center">P0[9]</td>
+ * <td align="center">P0[8]</td>
+ * <td align="center">P0[7]</td>
+ * <td align="center">P0[6]</td>
+ * <td align="center">P0[5]</td>
+ * <td align="center">P0[4]</td>
+ * <td align="center">P0[3]</td>
+ * <td align="center">P0[2]</td>
+ * </tr>
+ * <tr>
+ * <td align="center">Byte 1:</td>
+ * <td align="center">P1[9]</td>
+ * <td align="center">P1[8]</td>
+ * <td align="center">P1[7]</td>
+ * <td align="center">P1[6]</td>
+ * <td align="center">P1[5]</td>
+ * <td align="center">P1[4]</td>
+ * <td align="center">P1[3]</td>
+ * <td align="center">P1[2]</td>
+ * </tr>
+ * <tr>
+ * <td align="center">Byte 2:</td>
+ * <td align="center">P2[9]</td>
+ * <td align="center">P2[8]</td>
+ * <td align="center">P2[7]</td>
+ * <td align="center">P2[6]</td>
+ * <td align="center">P2[5]</td>
+ * <td align="center">P2[4]</td>
+ * <td align="center">P2[3]</td>
+ * <td align="center">P2[2]</td>
+ * </tr>
+ * <tr>
+ * <td align="center">Byte 3:</td>
+ * <td align="center">P3[9]</td>
+ * <td align="center">P3[8]</td>
+ * <td align="center">P3[7]</td>
+ * <td align="center">P3[6]</td>
+ * <td align="center">P3[5]</td>
+ * <td align="center">P3[4]</td>
+ * <td align="center">P3[3]</td>
+ * <td align="center">P3[2]</td>
+ * </tr>
+ * <tr>
+ * <td align="center">Byte 4:</td>
+ * <td align="center">P3[1]</td>
+ * <td align="center">P3[0]</td>
+ * <td align="center">P2[1]</td>
+ * <td align="center">P2[0]</td>
+ * <td align="center">P1[1]</td>
+ * <td align="center">P1[0]</td>
+ * <td align="center">P0[1]</td>
+ * <td align="center">P0[0]</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ * <p>
+ * This format assumes
+ * <ul>
+ * <li>a width multiple of 4 pixels</li>
+ * <li>an even height</li>
+ * </ul>
+ * </p>
+ *
+ * <pre>size = row stride * height</pre> where the row stride is in <em>bytes</em>,
+ * not pixels.
+ *
+ * <p>
+ * Since this is a densely packed format, the pixel stride is always 0. The
+ * application must use the pixel data layout defined in above table to
+ * access each row data. When row stride is equal to {@code width * (10 / 8)}, there
+ * will be no padding bytes at the end of each row, the entire image data is
+ * densely packed. When stride is larger than {@code width * (10 / 8)}, padding
+ * bytes will be present at the end of each row.
+ * </p>
+ * <p>
+ * For example, the {@link android.media.Image} object can provide data in
+ * this format from a {@link android.hardware.camera2.CameraDevice} (if
+ * supported) through a {@link android.media.ImageReader} object. The
+ * {@link android.media.Image#getPlanes() Image#getPlanes()} will return a
+ * single plane containing the pixel data. The pixel stride is always 0 in
+ * {@link android.media.Image.Plane#getPixelStride()}, and the
+ * {@link android.media.Image.Plane#getRowStride()} describes the vertical
+ * neighboring pixel distance (in bytes) between adjacent rows.
+ * </p>
+ *
+ * @see android.media.Image
+ * @see android.media.ImageReader
+ * @see android.hardware.camera2.CameraDevice
+ */
+ public static final int RAW10 = 0x25;
+
+ /**
+ * <p>
+ * Android 12-bit raw format
+ * </p>
+ * <p>
+ * This is a single-plane, 12-bit per pixel, densely packed (in each row),
+ * unprocessed format, usually representing raw Bayer-pattern images coming
+ * from an image sensor.
+ * </p>
+ * <p>
+ * In an image buffer with this format, starting from the first pixel of each
+ * row, each two consecutive pixels are packed into 3 bytes (24 bits). The first
+ * and second byte contains the top 8 bits of first and second pixel. The third
+ * byte contains the 4 least significant bits of the two pixels, the exact layout
+ * data for each two consecutive pixels is illustrated below (Pi[j] stands for
+ * the jth bit of the ith pixel):
+ * </p>
+ * <table>
+ * <thead>
+ * <tr>
+ * <th align="center"></th>
+ * <th align="center">bit 7</th>
+ * <th align="center">bit 6</th>
+ * <th align="center">bit 5</th>
+ * <th align="center">bit 4</th>
+ * <th align="center">bit 3</th>
+ * <th align="center">bit 2</th>
+ * <th align="center">bit 1</th>
+ * <th align="center">bit 0</th>
+ * </tr>
+ * </thead> <tbody>
+ * <tr>
+ * <td align="center">Byte 0:</td>
+ * <td align="center">P0[11]</td>
+ * <td align="center">P0[10]</td>
+ * <td align="center">P0[ 9]</td>
+ * <td align="center">P0[ 8]</td>
+ * <td align="center">P0[ 7]</td>
+ * <td align="center">P0[ 6]</td>
+ * <td align="center">P0[ 5]</td>
+ * <td align="center">P0[ 4]</td>
+ * </tr>
+ * <tr>
+ * <td align="center">Byte 1:</td>
+ * <td align="center">P1[11]</td>
+ * <td align="center">P1[10]</td>
+ * <td align="center">P1[ 9]</td>
+ * <td align="center">P1[ 8]</td>
+ * <td align="center">P1[ 7]</td>
+ * <td align="center">P1[ 6]</td>
+ * <td align="center">P1[ 5]</td>
+ * <td align="center">P1[ 4]</td>
+ * </tr>
+ * <tr>
+ * <td align="center">Byte 2:</td>
+ * <td align="center">P1[ 3]</td>
+ * <td align="center">P1[ 2]</td>
+ * <td align="center">P1[ 1]</td>
+ * <td align="center">P1[ 0]</td>
+ * <td align="center">P0[ 3]</td>
+ * <td align="center">P0[ 2]</td>
+ * <td align="center">P0[ 1]</td>
+ * <td align="center">P0[ 0]</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ * <p>
+ * This format assumes
+ * <ul>
+ * <li>a width multiple of 4 pixels</li>
+ * <li>an even height</li>
+ * </ul>
+ * </p>
+ *
+ * <pre>size = row stride * height</pre> where the row stride is in <em>bytes</em>,
+ * not pixels.
+ *
+ * <p>
+ * Since this is a densely packed format, the pixel stride is always 0. The
+ * application must use the pixel data layout defined in above table to
+ * access each row data. When row stride is equal to {@code width * (12 / 8)}, there
+ * will be no padding bytes at the end of each row, the entire image data is
+ * densely packed. When stride is larger than {@code width * (12 / 8)}, padding
+ * bytes will be present at the end of each row.
+ * </p>
+ * <p>
+ * For example, the {@link android.media.Image} object can provide data in
+ * this format from a {@link android.hardware.camera2.CameraDevice} (if
+ * supported) through a {@link android.media.ImageReader} object. The
+ * {@link android.media.Image#getPlanes() Image#getPlanes()} will return a
+ * single plane containing the pixel data. The pixel stride is always 0 in
+ * {@link android.media.Image.Plane#getPixelStride()}, and the
+ * {@link android.media.Image.Plane#getRowStride()} describes the vertical
+ * neighboring pixel distance (in bytes) between adjacent rows.
+ * </p>
+ *
+ * @see android.media.Image
+ * @see android.media.ImageReader
+ * @see android.hardware.camera2.CameraDevice
+ */
+ public static final int RAW12 = 0x26;
+
+ /**
+ * <p>Android dense depth image format.</p>
+ *
+ * <p>Each pixel is 16 bits, representing a depth ranging measurement from a depth camera or
+ * similar sensor. The 16-bit sample consists of a confidence value and the actual ranging
+ * measurement.</p>
+ *
+ * <p>The confidence value is an estimate of correctness for this sample. It is encoded in the
+ * 3 most significant bits of the sample, with a value of 0 representing 100% confidence, a
+ * value of 1 representing 0% confidence, a value of 2 representing 1/7, a value of 3
+ * representing 2/7, and so on.</p>
+ *
+ * <p>As an example, the following sample extracts the range and confidence from the first pixel
+ * of a DEPTH16-format {@link android.media.Image}, and converts the confidence to a
+ * floating-point value between 0 and 1.f inclusive, with 1.f representing maximum confidence:
+ *
+ * <pre>
+ * ShortBuffer shortDepthBuffer = img.getPlanes()[0].getBuffer().asShortBuffer();
+ * short depthSample = shortDepthBuffer.get()
+ * short depthRange = (short) (depthSample & 0x1FFF);
+ * short depthConfidence = (short) ((depthSample >> 13) & 0x7);
+ * float depthPercentage = depthConfidence == 0 ? 1.f : (depthConfidence - 1) / 7.f;
+ * </pre>
+ * </p>
+ *
+ * <p>This format assumes
+ * <ul>
+ * <li>an even width</li>
+ * <li>an even height</li>
+ * <li>a horizontal stride multiple of 16 pixels</li>
+ * </ul>
+ * </p>
+ *
+ * <pre> y_size = stride * height </pre>
+ *
+ * When produced by a camera, the units for the range are millimeters.
+ */
+ public static final int DEPTH16 = 0x44363159;
+
+ /**
+ * Android sparse depth point cloud format.
+ *
+ * <p>A variable-length list of 3D points plus a confidence value, with each point represented
+ * by four floats; first the X, Y, Z position coordinates, and then the confidence value.</p>
+ *
+ * <p>The number of points is {@code (size of the buffer in bytes) / 16}.
+ *
+ * <p>The coordinate system and units of the position values depend on the source of the point
+ * cloud data. The confidence value is between 0.f and 1.f, inclusive, with 0 representing 0%
+ * confidence and 1.f representing 100% confidence in the measured position values.</p>
+ *
+ * <p>As an example, the following code extracts the first depth point in a DEPTH_POINT_CLOUD
+ * format {@link android.media.Image}:
+ * <pre>
+ * FloatBuffer floatDepthBuffer = img.getPlanes()[0].getBuffer().asFloatBuffer();
+ * float x = floatDepthBuffer.get();
+ * float y = floatDepthBuffer.get();
+ * float z = floatDepthBuffer.get();
+ * float confidence = floatDepthBuffer.get();
+ * </pre>
+ *
+ */
+ public static final int DEPTH_POINT_CLOUD = 0x101;
+
+ /**
+ * Unprocessed implementation-dependent raw
+ * depth measurements, opaque with 16 bit
+ * samples.
+ *
+ * @hide
+ */
+ public static final int RAW_DEPTH = 0x1002;
+
+ /**
+ * Android private opaque image format.
+ * <p>
+ * The choices of the actual format and pixel data layout are entirely up to
+ * the device-specific and framework internal implementations, and may vary
+ * depending on use cases even for the same device. The buffers of this
+ * format can be produced by components like
+ * {@link android.media.ImageWriter ImageWriter} , and interpreted correctly
+ * by consumers like {@link android.hardware.camera2.CameraDevice
+ * CameraDevice} based on the device/framework private information. However,
+ * these buffers are not directly accessible to the application.
+ * </p>
+ * <p>
+ * When an {@link android.media.Image Image} of this format is obtained from
+ * an {@link android.media.ImageReader ImageReader} or
+ * {@link android.media.ImageWriter ImageWriter}, the
+ * {@link android.media.Image#getPlanes() getPlanes()} method will return an
+ * empty {@link android.media.Image.Plane Plane} array.
+ * </p>
+ * <p>
+ * If a buffer of this format is to be used as an OpenGL ES texture, the
+ * framework will assume that sampling the texture will always return an
+ * alpha value of 1.0 (i.e. the buffer contains only opaque pixel values).
+ * </p>
+ */
+ public static final int PRIVATE = 0x22;
+
+ /**
+ * Use this function to retrieve the number of bits per pixel of an
+ * ImageFormat.
+ *
+ * @param format
+ * @return the number of bits per pixel of the given format or -1 if the
+ * format doesn't exist or is not supported.
+ */
+ public static int getBitsPerPixel(int format) {
+ switch (format) {
+ case RGB_565:
+ return 16;
+ case NV16:
+ return 16;
+ case YUY2:
+ return 16;
+ case YV12:
+ return 12;
+ case Y8:
+ return 8;
+ case Y16:
+ case DEPTH16:
+ return 16;
+ case NV21:
+ return 12;
+ case YUV_420_888:
+ return 12;
+ case YUV_422_888:
+ return 16;
+ case YUV_444_888:
+ return 24;
+ case FLEX_RGB_888:
+ return 24;
+ case FLEX_RGBA_8888:
+ return 32;
+ case RAW_DEPTH:
+ case RAW_SENSOR:
+ return 16;
+ case RAW10:
+ return 10;
+ case RAW12:
+ return 12;
+ }
+ return -1;
+ }
+
+ /**
+ * Determine whether or not this is a public-visible {@code format}.
+ *
+ * <p>In particular, {@code @hide} formats will return {@code false}.</p>
+ *
+ * <p>Any other formats (including UNKNOWN) will return {@code false}.</p>
+ *
+ * @param format an integer format
+ * @return a boolean
+ *
+ * @hide
+ */
+ public static boolean isPublicFormat(int format) {
+ switch (format) {
+ case RGB_565:
+ case NV16:
+ case YUY2:
+ case YV12:
+ case JPEG:
+ case NV21:
+ case YUV_420_888:
+ case YUV_422_888:
+ case YUV_444_888:
+ case FLEX_RGB_888:
+ case FLEX_RGBA_8888:
+ case RAW_SENSOR:
+ case RAW_PRIVATE:
+ case RAW10:
+ case RAW12:
+ case DEPTH16:
+ case DEPTH_POINT_CLOUD:
+ case PRIVATE:
+ case RAW_DEPTH:
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/android/graphics/Insets.java b/android/graphics/Insets.java
new file mode 100644
index 0000000..156f990
--- /dev/null
+++ b/android/graphics/Insets.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * An Insets instance holds four integer offsets which describe changes to the four
+ * edges of a Rectangle. By convention, positive values move edges towards the
+ * centre of the rectangle.
+ * <p>
+ * Insets are immutable so may be treated as values.
+ *
+ * @hide
+ */
+public class Insets {
+ public static final Insets NONE = new Insets(0, 0, 0, 0);
+
+ public final int left;
+ public final int top;
+ public final int right;
+ public final int bottom;
+
+ private Insets(int left, int top, int right, int bottom) {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+
+ // Factory methods
+
+ /**
+ * Return an Insets instance with the appropriate values.
+ *
+ * @param left the left inset
+ * @param top the top inset
+ * @param right the right inset
+ * @param bottom the bottom inset
+ *
+ * @return Insets instance with the appropriate values
+ */
+ public static Insets of(int left, int top, int right, int bottom) {
+ if (left == 0 && top == 0 && right == 0 && bottom == 0) {
+ return NONE;
+ }
+ return new Insets(left, top, right, bottom);
+ }
+
+ /**
+ * Return an Insets instance with the appropriate values.
+ *
+ * @param r the rectangle from which to take the values
+ *
+ * @return an Insets instance with the appropriate values
+ */
+ public static Insets of(Rect r) {
+ return (r == null) ? NONE : of(r.left, r.top, r.right, r.bottom);
+ }
+
+ /**
+ * Two Insets instances are equal iff they belong to the same class and their fields are
+ * pairwise equal.
+ *
+ * @param o the object to compare this instance with.
+ *
+ * @return true iff this object is equal {@code o}
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Insets insets = (Insets) o;
+
+ if (bottom != insets.bottom) return false;
+ if (left != insets.left) return false;
+ if (right != insets.right) return false;
+ if (top != insets.top) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = left;
+ result = 31 * result + top;
+ result = 31 * result + right;
+ result = 31 * result + bottom;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Insets{" +
+ "left=" + left +
+ ", top=" + top +
+ ", right=" + right +
+ ", bottom=" + bottom +
+ '}';
+ }
+}
diff --git a/android/graphics/Interpolator.java b/android/graphics/Interpolator.java
new file mode 100644
index 0000000..1045464
--- /dev/null
+++ b/android/graphics/Interpolator.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import android.os.SystemClock;
+
+public class Interpolator {
+
+ public Interpolator(int valueCount) {
+ mValueCount = valueCount;
+ mFrameCount = 2;
+ native_instance = nativeConstructor(valueCount, 2);
+ }
+
+ public Interpolator(int valueCount, int frameCount) {
+ mValueCount = valueCount;
+ mFrameCount = frameCount;
+ native_instance = nativeConstructor(valueCount, frameCount);
+ }
+
+ /**
+ * Reset the Interpolator to have the specified number of values and an
+ * implicit keyFrame count of 2 (just a start and end). After this call the
+ * values for each keyFrame must be assigned using setKeyFrame().
+ */
+ public void reset(int valueCount) {
+ reset(valueCount, 2);
+ }
+
+ /**
+ * Reset the Interpolator to have the specified number of values and
+ * keyFrames. After this call the values for each keyFrame must be assigned
+ * using setKeyFrame().
+ */
+ public void reset(int valueCount, int frameCount) {
+ mValueCount = valueCount;
+ mFrameCount = frameCount;
+ nativeReset(native_instance, valueCount, frameCount);
+ }
+
+ public final int getKeyFrameCount() {
+ return mFrameCount;
+ }
+
+ public final int getValueCount() {
+ return mValueCount;
+ }
+
+ /**
+ * Assign the keyFrame (specified by index) a time value and an array of key
+ * values (with an implicity blend array of [0, 0, 1, 1] giving linear
+ * transition to the next set of key values).
+ *
+ * @param index The index of the key frame to assign
+ * @param msec The time (in mililiseconds) for this key frame. Based on the
+ * SystemClock.uptimeMillis() clock
+ * @param values Array of values associated with theis key frame
+ */
+ public void setKeyFrame(int index, int msec, float[] values) {
+ setKeyFrame(index, msec, values, null);
+ }
+
+ /**
+ * Assign the keyFrame (specified by index) a time value and an array of key
+ * values and blend array.
+ *
+ * @param index The index of the key frame to assign
+ * @param msec The time (in mililiseconds) for this key frame. Based on the
+ * SystemClock.uptimeMillis() clock
+ * @param values Array of values associated with theis key frame
+ * @param blend (may be null) Optional array of 4 blend values
+ */
+ public void setKeyFrame(int index, int msec, float[] values, float[] blend) {
+ if (index < 0 || index >= mFrameCount) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (values.length < mValueCount) {
+ throw new ArrayStoreException();
+ }
+ if (blend != null && blend.length < 4) {
+ throw new ArrayStoreException();
+ }
+ nativeSetKeyFrame(native_instance, index, msec, values, blend);
+ }
+
+ /**
+ * Set a repeat count (which may be fractional) for the interpolator, and
+ * whether the interpolator should mirror its repeats. The default settings
+ * are repeatCount = 1, and mirror = false.
+ */
+ public void setRepeatMirror(float repeatCount, boolean mirror) {
+ if (repeatCount >= 0) {
+ nativeSetRepeatMirror(native_instance, repeatCount, mirror);
+ }
+ }
+
+ public enum Result {
+ NORMAL,
+ FREEZE_START,
+ FREEZE_END
+ }
+
+ /**
+ * Calls timeToValues(msec, values) with the msec set to now (by calling
+ * (int)SystemClock.uptimeMillis().)
+ */
+ public Result timeToValues(float[] values) {
+ return timeToValues((int)SystemClock.uptimeMillis(), values);
+ }
+
+ /**
+ * Given a millisecond time value (msec), return the interpolated values and
+ * return whether the specified time was within the range of key times
+ * (NORMAL), was before the first key time (FREEZE_START) or after the last
+ * key time (FREEZE_END). In any event, computed values are always returned.
+ *
+ * @param msec The time (in milliseconds) used to sample into the
+ * Interpolator. Based on the SystemClock.uptimeMillis() clock
+ * @param values Where to write the computed values (may be NULL).
+ * @return how the values were computed (even if values == null)
+ */
+ public Result timeToValues(int msec, float[] values) {
+ if (values != null && values.length < mValueCount) {
+ throw new ArrayStoreException();
+ }
+ switch (nativeTimeToValues(native_instance, msec, values)) {
+ case 0: return Result.NORMAL;
+ case 1: return Result.FREEZE_START;
+ default: return Result.FREEZE_END;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ nativeDestructor(native_instance);
+ native_instance = 0; // Other finalizers can still call us.
+ }
+
+ private int mValueCount;
+ private int mFrameCount;
+ private long native_instance;
+
+ private static native long nativeConstructor(int valueCount, int frameCount);
+ private static native void nativeDestructor(long native_instance);
+ private static native void nativeReset(long native_instance, int valueCount, int frameCount);
+ private static native void nativeSetKeyFrame(long native_instance, int index, int msec, float[] values, float[] blend);
+ private static native void nativeSetRepeatMirror(long native_instance, float repeatCount, boolean mirror);
+ private static native int nativeTimeToValues(long native_instance, int msec, float[] values);
+}
+
diff --git a/android/graphics/LargeBitmap.java b/android/graphics/LargeBitmap.java
new file mode 100644
index 0000000..936c338
--- /dev/null
+++ b/android/graphics/LargeBitmap.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * LargeBitmap can be used to decode a rectangle region from an image.
+ * LargeBimap is particularly useful when an original image is large and
+ * you only need parts of the image.
+ *
+ * To create a LargeBitmap, call BitmapFactory.createLargeBitmap().
+ * Given a LargeBitmap, users can call decodeRegion() repeatedly
+ * to get a decoded Bitmap of the specified region.
+ * @hide
+ */
+public final class LargeBitmap {
+ private long mNativeLargeBitmap;
+ private boolean mRecycled;
+
+ /* Private constructor that must received an already allocated native
+ large bitmap int (pointer).
+
+ This can be called from JNI code.
+ */
+ private LargeBitmap(long nativeLbm) {
+ mNativeLargeBitmap = nativeLbm;
+ mRecycled = false;
+ }
+
+ /**
+ * Decodes a rectangle region in the image specified by rect.
+ *
+ * @param rect The rectangle that specified the region to be decode.
+ * @param opts null-ok; Options that control downsampling.
+ * inPurgeable is not supported.
+ * @return The decoded bitmap, or null if the image data could not be
+ * decoded.
+ */
+ public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) {
+ checkRecycled("decodeRegion called on recycled large bitmap");
+ if (rect.left < 0 || rect.top < 0 || rect.right > getWidth() || rect.bottom > getHeight())
+ throw new IllegalArgumentException("rectangle is not inside the image");
+ return nativeDecodeRegion(mNativeLargeBitmap, rect.left, rect.top,
+ rect.right - rect.left, rect.bottom - rect.top, options);
+ }
+
+ /** Returns the original image's width */
+ public int getWidth() {
+ checkRecycled("getWidth called on recycled large bitmap");
+ return nativeGetWidth(mNativeLargeBitmap);
+ }
+
+ /** Returns the original image's height */
+ public int getHeight() {
+ checkRecycled("getHeight called on recycled large bitmap");
+ return nativeGetHeight(mNativeLargeBitmap);
+ }
+
+ /**
+ * Frees up the memory associated with this large bitmap, and mark the
+ * large bitmap as "dead", meaning it will throw an exception if decodeRegion(),
+ * getWidth() or getHeight() is called.
+ * This operation cannot be reversed, so it should only be called if you are
+ * sure there are no further uses for the large bitmap. This is an advanced call,
+ * and normally need not be called, since the normal GC process will free up this
+ * memory when there are no more references to this bitmap.
+ */
+ public void recycle() {
+ if (!mRecycled) {
+ nativeClean(mNativeLargeBitmap);
+ mRecycled = true;
+ }
+ }
+
+ /**
+ * Returns true if this large bitmap has been recycled.
+ * If so, then it is an error to try use its method.
+ *
+ * @return true if the large bitmap has been recycled
+ */
+ public final boolean isRecycled() {
+ return mRecycled;
+ }
+
+ /**
+ * Called by methods that want to throw an exception if the bitmap
+ * has already been recycled.
+ */
+ private void checkRecycled(String errorMessage) {
+ if (mRecycled) {
+ throw new IllegalStateException(errorMessage);
+ }
+ }
+
+ protected void finalize() {
+ recycle();
+ }
+
+ private static native Bitmap nativeDecodeRegion(long nativeLbm,
+ int start_x, int start_y, int width, int height,
+ BitmapFactory.Options options);
+ private static native int nativeGetWidth(long nativeLbm);
+ private static native int nativeGetHeight(long nativeLbm);
+ private static native void nativeClean(long nativeLbm);
+}
diff --git a/android/graphics/LayerRasterizer.java b/android/graphics/LayerRasterizer.java
new file mode 100644
index 0000000..25155ab
--- /dev/null
+++ b/android/graphics/LayerRasterizer.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * @removed feature is not supported by hw-accerlerated or PDF backends
+ */
+@Deprecated
+public class LayerRasterizer extends Rasterizer {
+ public LayerRasterizer() { }
+
+ /** Add a new layer (above any previous layers) to the rasterizer.
+ The layer will extract those fields that affect the mask from
+ the specified paint, but will not retain a reference to the paint
+ object itself, so it may be reused without danger of side-effects.
+ */
+ public void addLayer(Paint paint, float dx, float dy) { }
+
+ public void addLayer(Paint paint) { }
+}
+
diff --git a/android/graphics/LeakyTypefaceStorage.java b/android/graphics/LeakyTypefaceStorage.java
new file mode 100644
index 0000000..618e60d
--- /dev/null
+++ b/android/graphics/LeakyTypefaceStorage.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2017 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 android.graphics;
+
+import com.android.internal.annotations.GuardedBy;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Process;
+import android.util.ArrayMap;
+
+import java.util.ArrayList;
+
+/**
+ * This class is used for Parceling Typeface object.
+ * Note: Typeface object can not be passed over the process boundary.
+ *
+ * @hide
+ */
+public class LeakyTypefaceStorage {
+ private static final Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ private static final ArrayList<Typeface> sStorage = new ArrayList<>();
+ @GuardedBy("sLock")
+ private static final ArrayMap<Typeface, Integer> sTypefaceMap = new ArrayMap<>();
+
+ /**
+ * Write typeface to parcel.
+ *
+ * You can't transfer Typeface to a different process. {@link readTypefaceFromParcel} will
+ * return {@code null} if the {@link readTypefaceFromParcel} is called in a different process.
+ *
+ * @param typeface A {@link Typeface} to be written.
+ * @param parcel A {@link Parcel} object.
+ */
+ public static void writeTypefaceToParcel(@Nullable Typeface typeface, @NonNull Parcel parcel) {
+ parcel.writeInt(Process.myPid());
+ synchronized (sLock) {
+ final int id;
+ final Integer i = sTypefaceMap.get(typeface);
+ if (i != null) {
+ id = i.intValue();
+ } else {
+ id = sStorage.size();
+ sStorage.add(typeface);
+ sTypefaceMap.put(typeface, id);
+ }
+ parcel.writeInt(id);
+ }
+ }
+
+ /**
+ * Read typeface from parcel.
+ *
+ * If the {@link Typeface} was created in another process, this method returns null.
+ *
+ * @param parcel A {@link Parcel} object
+ * @return A {@link Typeface} object.
+ */
+ public static @Nullable Typeface readTypefaceFromParcel(@NonNull Parcel parcel) {
+ final int pid = parcel.readInt();
+ final int typefaceId = parcel.readInt();
+ if (pid != Process.myPid()) {
+ return null; // The Typeface was created and written in another process.
+ }
+ synchronized (sLock) {
+ return sStorage.get(typefaceId);
+ }
+ }
+}
diff --git a/android/graphics/LightingColorFilter.java b/android/graphics/LightingColorFilter.java
new file mode 100644
index 0000000..1578ffb
--- /dev/null
+++ b/android/graphics/LightingColorFilter.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2006 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.
+ */
+
+// This file was generated from the C++ include file: SkColorFilter.h
+// Any changes made to this file will be discarded by the build.
+// To change this file, either edit the include, or device/tools/gluemaker/main.cpp,
+// or one of the auxilary file specifications in device/tools/gluemaker.
+
+package android.graphics;
+
+import android.annotation.ColorInt;
+
+/**
+ * A color filter that can be used to simulate simple lighting effects.
+ * A <code>LightingColorFilter</code> is defined by two parameters, one
+ * used to multiply the source color (called <code>colorMultiply</code>)
+ * and one used to add to the source color (called <code>colorAdd</code>).
+ * The alpha channel is left untouched by this color filter.
+ *
+ * Given a source color RGB, the resulting R'G'B' color is computed thusly:
+ * <pre>
+ * R' = R * colorMultiply.R + colorAdd.R
+ * G' = G * colorMultiply.G + colorAdd.G
+ * B' = B * colorMultiply.B + colorAdd.B
+ * </pre>
+ * The result is pinned to the <code>[0..255]</code> range for each channel.
+ */
+public class LightingColorFilter extends ColorFilter {
+ @ColorInt
+ private int mMul;
+ @ColorInt
+ private int mAdd;
+
+ /**
+ * Create a colorfilter that multiplies the RGB channels by one color,
+ * and then adds a second color. The alpha components of the mul and add
+ * arguments are ignored.
+ */
+ public LightingColorFilter(@ColorInt int mul, @ColorInt int add) {
+ mMul = mul;
+ mAdd = add;
+ }
+
+ /**
+ * Returns the RGB color used to multiply the source color when the
+ * color filter is applied.
+ */
+ @ColorInt
+ public int getColorMultiply() {
+ return mMul;
+ }
+
+ /**
+ * Specifies the RGB color used to multiply the source color when the
+ * color filter is applied.
+ * The alpha channel of this color is ignored.
+ *
+ * @see #getColorMultiply()
+ *
+ * @hide
+ */
+ public void setColorMultiply(@ColorInt int mul) {
+ if (mMul != mul) {
+ mMul = mul;
+ discardNativeInstance();
+ }
+ }
+
+ /**
+ * Returns the RGB color that will be added to the source color
+ * when the color filter is applied.
+ */
+ @ColorInt
+ public int getColorAdd() {
+ return mAdd;
+ }
+
+ /**
+ * Specifies the RGB that will be added to the source color when
+ * the color filter is applied.
+ * The alpha channel of this color is ignored.
+ *
+ * @see #getColorAdd()
+ *
+ * @hide
+ */
+ public void setColorAdd(@ColorInt int add) {
+ if (mAdd != add) {
+ mAdd = add;
+ discardNativeInstance();
+ }
+ }
+
+ @Override
+ long createNativeInstance() {
+ return native_CreateLightingFilter(mMul, mAdd);
+ }
+
+ private static native long native_CreateLightingFilter(int mul, int add);
+}
diff --git a/android/graphics/LightingColorFilter_Delegate.java b/android/graphics/LightingColorFilter_Delegate.java
new file mode 100644
index 0000000..0dd9703
--- /dev/null
+++ b/android/graphics/LightingColorFilter_Delegate.java
@@ -0,0 +1,59 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.LightingColorFilter
+ *
+ * Through the layoutlib_create tool, the original native methods of LightingColorFilter have
+ * been replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original LightingColorFilter class.
+ *
+ * Because this extends {@link ColorFilter_Delegate}, there's no need to use a
+ * {@link DelegateManager}, as all the Shader classes will be added to the manager
+ * owned by {@link ColorFilter_Delegate}.
+ *
+ * @see ColorFilter_Delegate
+ *
+ */
+public class LightingColorFilter_Delegate extends ColorFilter_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public String getSupportMessage() {
+ return "Lighting Color Filters are not supported.";
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long native_CreateLightingFilter(int mul, int add) {
+ LightingColorFilter_Delegate newDelegate = new LightingColorFilter_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/LinearGradient.java b/android/graphics/LinearGradient.java
new file mode 100644
index 0000000..7139efe
--- /dev/null
+++ b/android/graphics/LinearGradient.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+public class LinearGradient extends Shader {
+
+ private static final int TYPE_COLORS_AND_POSITIONS = 1;
+ private static final int TYPE_COLOR_START_AND_COLOR_END = 2;
+
+ /**
+ * Type of the LinearGradient: can be either TYPE_COLORS_AND_POSITIONS or
+ * TYPE_COLOR_START_AND_COLOR_END.
+ */
+ private int mType;
+
+ private float mX0;
+ private float mY0;
+ private float mX1;
+ private float mY1;
+ private int[] mColors;
+ private float[] mPositions;
+ private int mColor0;
+ private int mColor1;
+
+ private TileMode mTileMode;
+
+ /**
+ * Create a shader that draws a linear gradient along a line.
+ *
+ * @param x0 The x-coordinate for the start of the gradient line
+ * @param y0 The y-coordinate for the start of the gradient line
+ * @param x1 The x-coordinate for the end of the gradient line
+ * @param y1 The y-coordinate for the end of the gradient line
+ * @param colors The colors to be distributed along the gradient line
+ * @param positions May be null. The relative positions [0..1] of
+ * each corresponding color in the colors array. If this is null,
+ * the the colors are distributed evenly along the gradient line.
+ * @param tile The Shader tiling mode
+ */
+ public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int colors[],
+ @Nullable float positions[], @NonNull TileMode tile) {
+ if (colors.length < 2) {
+ throw new IllegalArgumentException("needs >= 2 number of colors");
+ }
+ if (positions != null && colors.length != positions.length) {
+ throw new IllegalArgumentException("color and position arrays must be of equal length");
+ }
+ mType = TYPE_COLORS_AND_POSITIONS;
+ mX0 = x0;
+ mY0 = y0;
+ mX1 = x1;
+ mY1 = y1;
+ mColors = colors.clone();
+ mPositions = positions != null ? positions.clone() : null;
+ mTileMode = tile;
+ }
+
+ /**
+ * Create a shader that draws a linear gradient along a line.
+ *
+ * @param x0 The x-coordinate for the start of the gradient line
+ * @param y0 The y-coordinate for the start of the gradient line
+ * @param x1 The x-coordinate for the end of the gradient line
+ * @param y1 The y-coordinate for the end of the gradient line
+ * @param color0 The color at the start of the gradient line.
+ * @param color1 The color at the end of the gradient line.
+ * @param tile The Shader tiling mode
+ */
+ public LinearGradient(float x0, float y0, float x1, float y1,
+ @ColorInt int color0, @ColorInt int color1,
+ @NonNull TileMode tile) {
+ mType = TYPE_COLOR_START_AND_COLOR_END;
+ mX0 = x0;
+ mY0 = y0;
+ mX1 = x1;
+ mY1 = y1;
+ mColor0 = color0;
+ mColor1 = color1;
+ mColors = null;
+ mPositions = null;
+ mTileMode = tile;
+ }
+
+ @Override
+ long createNativeInstance(long nativeMatrix) {
+ if (mType == TYPE_COLORS_AND_POSITIONS) {
+ return nativeCreate1(nativeMatrix, mX0, mY0, mX1, mY1,
+ mColors, mPositions, mTileMode.nativeInt);
+ } else { // TYPE_COLOR_START_AND_COLOR_END
+ return nativeCreate2(nativeMatrix, mX0, mY0, mX1, mY1,
+ mColor0, mColor1, mTileMode.nativeInt);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected Shader copy() {
+ final LinearGradient copy;
+ if (mType == TYPE_COLORS_AND_POSITIONS) {
+ copy = new LinearGradient(mX0, mY0, mX1, mY1, mColors.clone(),
+ mPositions != null ? mPositions.clone() : null, mTileMode);
+ } else { // TYPE_COLOR_START_AND_COLOR_END
+ copy = new LinearGradient(mX0, mY0, mX1, mY1, mColor0, mColor1, mTileMode);
+ }
+ copyLocalMatrix(copy);
+ return copy;
+ }
+
+ private native long nativeCreate1(long matrix, float x0, float y0, float x1, float y1,
+ int colors[], float positions[], int tileMode);
+ private native long nativeCreate2(long matrix, float x0, float y0, float x1, float y1,
+ int color0, int color1, int tileMode);
+}
diff --git a/android/graphics/LinearGradient_Delegate.java b/android/graphics/LinearGradient_Delegate.java
new file mode 100644
index 0000000..477705c
--- /dev/null
+++ b/android/graphics/LinearGradient_Delegate.java
@@ -0,0 +1,226 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.graphics.Shader.TileMode;
+
+import java.awt.image.ColorModel;
+import java.awt.image.DataBufferInt;
+import java.awt.image.Raster;
+import java.awt.image.SampleModel;
+
+/**
+ * Delegate implementing the native methods of android.graphics.LinearGradient
+ *
+ * Through the layoutlib_create tool, the original native methods of LinearGradient have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original LinearGradient class.
+ *
+ * Because this extends {@link Shader_Delegate}, there's no need to use a {@link DelegateManager},
+ * as all the Shader classes will be added to the manager owned by {@link Shader_Delegate}.
+ *
+ * @see Shader_Delegate
+ *
+ */
+public final class LinearGradient_Delegate extends Gradient_Delegate {
+
+ // ---- delegate data ----
+ private java.awt.Paint mJavaPaint;
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public java.awt.Paint getJavaPaint() {
+ return mJavaPaint;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate1(LinearGradient thisGradient, long matrix,
+ float x0, float y0, float x1, float y1,
+ int colors[], float positions[], int tileMode) {
+ LinearGradient_Delegate newDelegate = new LinearGradient_Delegate(matrix, x0, y0,
+ x1, y1, colors, positions, Shader_Delegate.getTileMode(tileMode));
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate2(LinearGradient thisGradient, long matrix,
+ float x0, float y0, float x1, float y1,
+ int color0, int color1, int tileMode) {
+ return nativeCreate1(thisGradient, matrix, x0, y0, x1, y1, new int[] { color0, color1},
+ null /*positions*/, tileMode);
+ }
+
+ // ---- Private delegate/helper methods ----
+
+ /**
+ * Create a shader that draws a linear gradient along a line.
+ *
+ * @param nativeMatrix reference to the shader's native transformation matrix
+ * @param x0 The x-coordinate for the start of the gradient line
+ * @param y0 The y-coordinate for the start of the gradient line
+ * @param x1 The x-coordinate for the end of the gradient line
+ * @param y1 The y-coordinate for the end of the gradient line
+ * @param colors The colors to be distributed along the gradient line
+ * @param positions May be null. The relative positions [0..1] of each
+ * corresponding color in the colors array. If this is null, the
+ * the colors are distributed evenly along the gradient line.
+ * @param tile The Shader tiling mode
+ */
+ private LinearGradient_Delegate(long nativeMatrix, float x0, float y0, float x1,
+ float y1, int colors[], float positions[], TileMode tile) {
+ super(nativeMatrix, colors, positions);
+ mJavaPaint = new LinearGradientPaint(x0, y0, x1, y1, mColors, mPositions, tile);
+ }
+
+ // ---- Custom Java Paint ----
+ /**
+ * Linear Gradient (Java) Paint able to handle more than 2 points, as
+ * {@link java.awt.GradientPaint} only supports 2 points and does not support Android's tile
+ * modes.
+ */
+ private class LinearGradientPaint extends GradientPaint {
+
+ private final float mX0;
+ private final float mY0;
+ private final float mDx;
+ private final float mDy;
+ private final float mDSize2;
+
+ public LinearGradientPaint(float x0, float y0, float x1, float y1, int colors[],
+ float positions[], TileMode tile) {
+ super(colors, positions, tile);
+ mX0 = x0;
+ mY0 = y0;
+ mDx = x1 - x0;
+ mDy = y1 - y0;
+ mDSize2 = mDx * mDx + mDy * mDy;
+ }
+
+ @Override
+ public java.awt.PaintContext createContext(
+ java.awt.image.ColorModel colorModel,
+ java.awt.Rectangle deviceBounds,
+ java.awt.geom.Rectangle2D userBounds,
+ java.awt.geom.AffineTransform xform,
+ java.awt.RenderingHints hints) {
+ precomputeGradientColors();
+
+ java.awt.geom.AffineTransform canvasMatrix;
+ try {
+ canvasMatrix = xform.createInverse();
+ } catch (java.awt.geom.NoninvertibleTransformException e) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE,
+ "Unable to inverse matrix in LinearGradient", e, null /*data*/);
+ canvasMatrix = new java.awt.geom.AffineTransform();
+ }
+
+ java.awt.geom.AffineTransform localMatrix = getLocalMatrix();
+ try {
+ localMatrix = localMatrix.createInverse();
+ } catch (java.awt.geom.NoninvertibleTransformException e) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE,
+ "Unable to inverse matrix in LinearGradient", e, null /*data*/);
+ localMatrix = new java.awt.geom.AffineTransform();
+ }
+
+ return new LinearGradientPaintContext(canvasMatrix, localMatrix, colorModel);
+ }
+
+ private class LinearGradientPaintContext implements java.awt.PaintContext {
+
+ private final java.awt.geom.AffineTransform mCanvasMatrix;
+ private final java.awt.geom.AffineTransform mLocalMatrix;
+ private final java.awt.image.ColorModel mColorModel;
+
+ private LinearGradientPaintContext(
+ java.awt.geom.AffineTransform canvasMatrix,
+ java.awt.geom.AffineTransform localMatrix,
+ java.awt.image.ColorModel colorModel) {
+ mCanvasMatrix = canvasMatrix;
+ mLocalMatrix = localMatrix;
+ mColorModel = colorModel.hasAlpha() ? colorModel : ColorModel.getRGBdefault();
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public java.awt.image.ColorModel getColorModel() {
+ return mColorModel;
+ }
+
+ @Override
+ public java.awt.image.Raster getRaster(int x, int y, int w, int h) {
+ int[] data = new int[w*h];
+
+ int index = 0;
+ float[] pt1 = new float[2];
+ float[] pt2 = new float[2];
+ for (int iy = 0 ; iy < h ; iy++) {
+ for (int ix = 0 ; ix < w ; ix++) {
+ // handle the canvas transform
+ pt1[0] = x + ix;
+ pt1[1] = y + iy;
+ mCanvasMatrix.transform(pt1, 0, pt2, 0, 1);
+
+ // handle the local matrix.
+ pt1[0] = pt2[0];
+ pt1[1] = pt2[1];
+ mLocalMatrix.transform(pt1, 0, pt2, 0, 1);
+
+ data[index++] = getColor(pt2[0], pt2[1]);
+ }
+ }
+
+ DataBufferInt dataBuffer = new DataBufferInt(data, data.length);
+ SampleModel colorModel = mColorModel.createCompatibleSampleModel(w, h);
+ return Raster.createWritableRaster(colorModel, dataBuffer, null);
+ }
+ }
+
+ /**
+ * Returns a color for an arbitrary point.
+ */
+ private int getColor(float x, float y) {
+ float pos;
+ if (mDx == 0) {
+ pos = (y - mY0) / mDy;
+ } else if (mDy == 0) {
+ pos = (x - mX0) / mDx;
+ } else {
+ // 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
+ pos = (_x - mX0) / mDx;
+ }
+
+ return getGradientColor(pos);
+ }
+ }
+}
diff --git a/android/graphics/MaskFilter.java b/android/graphics/MaskFilter.java
new file mode 100644
index 0000000..d474315
--- /dev/null
+++ b/android/graphics/MaskFilter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * MaskFilter is the base class for object that perform transformations on
+ * an alpha-channel mask before drawing it. A subclass of MaskFilter may be
+ * installed into a Paint. Blur and emboss are implemented as subclasses of MaskFilter.
+ */
+public class MaskFilter {
+
+ protected void finalize() throws Throwable {
+ nativeDestructor(native_instance);
+ native_instance = 0; // Other finalizers can still call us.
+ }
+
+ private static native void nativeDestructor(long native_filter);
+ long native_instance;
+}
diff --git a/android/graphics/MaskFilter_Delegate.java b/android/graphics/MaskFilter_Delegate.java
new file mode 100644
index 0000000..e726c59
--- /dev/null
+++ b/android/graphics/MaskFilter_Delegate.java
@@ -0,0 +1,64 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.MaskFilter
+ *
+ * Through the layoutlib_create tool, the original native methods of MaskFilter have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original MaskFilter class.
+ *
+ * This also serve as a base class for all MaskFilter delegate classes.
+ *
+ * @see DelegateManager
+ *
+ */
+public abstract class MaskFilter_Delegate {
+
+ // ---- delegate manager ----
+ protected static final DelegateManager<MaskFilter_Delegate> sManager =
+ new DelegateManager<MaskFilter_Delegate>(MaskFilter_Delegate.class);
+
+ // ---- delegate helper data ----
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ public static MaskFilter_Delegate getDelegate(long nativeShader) {
+ return sManager.getDelegate(nativeShader);
+ }
+
+ public abstract boolean isSupported();
+ public abstract String getSupportMessage();
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeDestructor(long native_filter) {
+ sManager.removeJavaReferenceFor(native_filter);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/Matrix.java b/android/graphics/Matrix.java
new file mode 100644
index 0000000..486070c
--- /dev/null
+++ b/android/graphics/Matrix.java
@@ -0,0 +1,943 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import dalvik.annotation.optimization.CriticalNative;
+import dalvik.annotation.optimization.FastNative;
+
+import libcore.util.NativeAllocationRegistry;
+
+import java.io.PrintWriter;
+
+/**
+ * The Matrix class holds a 3x3 matrix for transforming coordinates.
+ */
+public class Matrix {
+
+ public static final int MSCALE_X = 0; //!< use with getValues/setValues
+ public static final int MSKEW_X = 1; //!< use with getValues/setValues
+ public static final int MTRANS_X = 2; //!< use with getValues/setValues
+ public static final int MSKEW_Y = 3; //!< use with getValues/setValues
+ public static final int MSCALE_Y = 4; //!< use with getValues/setValues
+ public static final int MTRANS_Y = 5; //!< use with getValues/setValues
+ public static final int MPERSP_0 = 6; //!< use with getValues/setValues
+ public static final int MPERSP_1 = 7; //!< use with getValues/setValues
+ public static final int MPERSP_2 = 8; //!< use with getValues/setValues
+
+ /** @hide */
+ public final static Matrix IDENTITY_MATRIX = new Matrix() {
+ void oops() {
+ throw new IllegalStateException("Matrix can not be modified");
+ }
+
+ @Override
+ public void set(Matrix src) {
+ oops();
+ }
+
+ @Override
+ public void reset() {
+ oops();
+ }
+
+ @Override
+ public void setTranslate(float dx, float dy) {
+ oops();
+ }
+
+ @Override
+ public void setScale(float sx, float sy, float px, float py) {
+ oops();
+ }
+
+ @Override
+ public void setScale(float sx, float sy) {
+ oops();
+ }
+
+ @Override
+ public void setRotate(float degrees, float px, float py) {
+ oops();
+ }
+
+ @Override
+ public void setRotate(float degrees) {
+ oops();
+ }
+
+ @Override
+ public void setSinCos(float sinValue, float cosValue, float px, float py) {
+ oops();
+ }
+
+ @Override
+ public void setSinCos(float sinValue, float cosValue) {
+ oops();
+ }
+
+ @Override
+ public void setSkew(float kx, float ky, float px, float py) {
+ oops();
+ }
+
+ @Override
+ public void setSkew(float kx, float ky) {
+ oops();
+ }
+
+ @Override
+ public boolean setConcat(Matrix a, Matrix b) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preTranslate(float dx, float dy) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preScale(float sx, float sy, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preScale(float sx, float sy) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preRotate(float degrees, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preRotate(float degrees) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preSkew(float kx, float ky, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preSkew(float kx, float ky) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean preConcat(Matrix other) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postTranslate(float dx, float dy) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postScale(float sx, float sy, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postScale(float sx, float sy) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postRotate(float degrees, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postRotate(float degrees) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postSkew(float kx, float ky, float px, float py) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postSkew(float kx, float ky) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean postConcat(Matrix other) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public boolean setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex,
+ int pointCount) {
+ oops();
+ return false;
+ }
+
+ @Override
+ public void setValues(float[] values) {
+ oops();
+ }
+ };
+
+ // sizeof(SkMatrix) is 9 * sizeof(float) + uint32_t
+ private static final long NATIVE_ALLOCATION_SIZE = 40;
+
+ private static class NoImagePreloadHolder {
+ public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
+ Matrix.class.getClassLoader(), nGetNativeFinalizer(), NATIVE_ALLOCATION_SIZE);
+ }
+
+ /**
+ * @hide
+ */
+ public final long native_instance;
+
+ /**
+ * Create an identity matrix
+ */
+ public Matrix() {
+ native_instance = nCreate(0);
+ NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, native_instance);
+ }
+
+ /**
+ * Create a matrix that is a (deep) copy of src
+ *
+ * @param src The matrix to copy into this matrix
+ */
+ public Matrix(Matrix src) {
+ native_instance = nCreate(src != null ? src.native_instance : 0);
+ NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, native_instance);
+ }
+
+ /**
+ * Returns true if the matrix is identity. This maybe faster than testing if (getType() == 0)
+ */
+ public boolean isIdentity() {
+ return nIsIdentity(native_instance);
+ }
+
+ /**
+ * Gets whether this matrix is affine. An affine matrix preserves straight lines and has no
+ * perspective.
+ *
+ * @return Whether the matrix is affine.
+ */
+ public boolean isAffine() {
+ return nIsAffine(native_instance);
+ }
+
+ /**
+ * Returns true if will map a rectangle to another rectangle. This can be true if the matrix is
+ * identity, scale-only, or rotates a multiple of 90 degrees.
+ */
+ public boolean rectStaysRect() {
+ return nRectStaysRect(native_instance);
+ }
+
+ /**
+ * (deep) copy the src matrix into this matrix. If src is null, reset this matrix to the
+ * identity matrix.
+ */
+ public void set(Matrix src) {
+ if (src == null) {
+ reset();
+ } else {
+ nSet(native_instance, src.native_instance);
+ }
+ }
+
+ /**
+ * Returns true iff obj is a Matrix and its values equal our values.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ // if (obj == this) return true; -- NaN value would mean matrix != itself
+ if (!(obj instanceof Matrix)) {
+ return false;
+ }
+ return nEquals(native_instance, ((Matrix) obj).native_instance);
+ }
+
+ @Override
+ public int hashCode() {
+ // This should generate the hash code by performing some arithmetic operation on all
+ // the matrix elements -- our equals() does an element-by-element comparison, and we
+ // need to ensure that the hash code for two equal objects is the same. We're not
+ // really using this at the moment, so we take the easy way out.
+ return 44;
+ }
+
+ /** Set the matrix to identity */
+ public void reset() {
+ nReset(native_instance);
+ }
+
+ /** Set the matrix to translate by (dx, dy). */
+ public void setTranslate(float dx, float dy) {
+ nSetTranslate(native_instance, dx, dy);
+ }
+
+ /**
+ * Set the matrix to scale by sx and sy, with a pivot point at (px, py). The pivot point is the
+ * coordinate that should remain unchanged by the specified transformation.
+ */
+ public void setScale(float sx, float sy, float px, float py) {
+ nSetScale(native_instance, sx, sy, px, py);
+ }
+
+ /** Set the matrix to scale by sx and sy. */
+ public void setScale(float sx, float sy) {
+ nSetScale(native_instance, sx, sy);
+ }
+
+ /**
+ * Set the matrix to rotate by the specified number of degrees, with a pivot point at (px, py).
+ * The pivot point is the coordinate that should remain unchanged by the specified
+ * transformation.
+ */
+ public void setRotate(float degrees, float px, float py) {
+ nSetRotate(native_instance, degrees, px, py);
+ }
+
+ /**
+ * Set the matrix to rotate about (0,0) by the specified number of degrees.
+ */
+ public void setRotate(float degrees) {
+ nSetRotate(native_instance, degrees);
+ }
+
+ /**
+ * Set the matrix to rotate by the specified sine and cosine values, with a pivot point at (px,
+ * py). The pivot point is the coordinate that should remain unchanged by the specified
+ * transformation.
+ */
+ public void setSinCos(float sinValue, float cosValue, float px, float py) {
+ nSetSinCos(native_instance, sinValue, cosValue, px, py);
+ }
+
+ /** Set the matrix to rotate by the specified sine and cosine values. */
+ public void setSinCos(float sinValue, float cosValue) {
+ nSetSinCos(native_instance, sinValue, cosValue);
+ }
+
+ /**
+ * Set the matrix to skew by sx and sy, with a pivot point at (px, py). The pivot point is the
+ * coordinate that should remain unchanged by the specified transformation.
+ */
+ public void setSkew(float kx, float ky, float px, float py) {
+ nSetSkew(native_instance, kx, ky, px, py);
+ }
+
+ /** Set the matrix to skew by sx and sy. */
+ public void setSkew(float kx, float ky) {
+ nSetSkew(native_instance, kx, ky);
+ }
+
+ /**
+ * Set the matrix to the concatenation of the two specified matrices and return true.
+ * <p>
+ * Either of the two matrices may also be the target matrix, that is
+ * <code>matrixA.setConcat(matrixA, matrixB);</code> is valid.
+ * </p>
+ * <p class="note">
+ * In {@link android.os.Build.VERSION_CODES#GINGERBREAD_MR1} and below, this function returns
+ * true only if the result can be represented. In
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and above, it always returns true.
+ * </p>
+ */
+ public boolean setConcat(Matrix a, Matrix b) {
+ nSetConcat(native_instance, a.native_instance, b.native_instance);
+ return true;
+ }
+
+ /**
+ * Preconcats the matrix with the specified translation. M' = M * T(dx, dy)
+ */
+ public boolean preTranslate(float dx, float dy) {
+ nPreTranslate(native_instance, dx, dy);
+ return true;
+ }
+
+ /**
+ * Preconcats the matrix with the specified scale. M' = M * S(sx, sy, px, py)
+ */
+ public boolean preScale(float sx, float sy, float px, float py) {
+ nPreScale(native_instance, sx, sy, px, py);
+ return true;
+ }
+
+ /**
+ * Preconcats the matrix with the specified scale. M' = M * S(sx, sy)
+ */
+ public boolean preScale(float sx, float sy) {
+ nPreScale(native_instance, sx, sy);
+ return true;
+ }
+
+ /**
+ * Preconcats the matrix with the specified rotation. M' = M * R(degrees, px, py)
+ */
+ public boolean preRotate(float degrees, float px, float py) {
+ nPreRotate(native_instance, degrees, px, py);
+ return true;
+ }
+
+ /**
+ * Preconcats the matrix with the specified rotation. M' = M * R(degrees)
+ */
+ public boolean preRotate(float degrees) {
+ nPreRotate(native_instance, degrees);
+ return true;
+ }
+
+ /**
+ * Preconcats the matrix with the specified skew. M' = M * K(kx, ky, px, py)
+ */
+ public boolean preSkew(float kx, float ky, float px, float py) {
+ nPreSkew(native_instance, kx, ky, px, py);
+ return true;
+ }
+
+ /**
+ * Preconcats the matrix with the specified skew. M' = M * K(kx, ky)
+ */
+ public boolean preSkew(float kx, float ky) {
+ nPreSkew(native_instance, kx, ky);
+ return true;
+ }
+
+ /**
+ * Preconcats the matrix with the specified matrix. M' = M * other
+ */
+ public boolean preConcat(Matrix other) {
+ nPreConcat(native_instance, other.native_instance);
+ return true;
+ }
+
+ /**
+ * Postconcats the matrix with the specified translation. M' = T(dx, dy) * M
+ */
+ public boolean postTranslate(float dx, float dy) {
+ nPostTranslate(native_instance, dx, dy);
+ return true;
+ }
+
+ /**
+ * Postconcats the matrix with the specified scale. M' = S(sx, sy, px, py) * M
+ */
+ public boolean postScale(float sx, float sy, float px, float py) {
+ nPostScale(native_instance, sx, sy, px, py);
+ return true;
+ }
+
+ /**
+ * Postconcats the matrix with the specified scale. M' = S(sx, sy) * M
+ */
+ public boolean postScale(float sx, float sy) {
+ nPostScale(native_instance, sx, sy);
+ return true;
+ }
+
+ /**
+ * Postconcats the matrix with the specified rotation. M' = R(degrees, px, py) * M
+ */
+ public boolean postRotate(float degrees, float px, float py) {
+ nPostRotate(native_instance, degrees, px, py);
+ return true;
+ }
+
+ /**
+ * Postconcats the matrix with the specified rotation. M' = R(degrees) * M
+ */
+ public boolean postRotate(float degrees) {
+ nPostRotate(native_instance, degrees);
+ return true;
+ }
+
+ /**
+ * Postconcats the matrix with the specified skew. M' = K(kx, ky, px, py) * M
+ */
+ public boolean postSkew(float kx, float ky, float px, float py) {
+ nPostSkew(native_instance, kx, ky, px, py);
+ return true;
+ }
+
+ /**
+ * Postconcats the matrix with the specified skew. M' = K(kx, ky) * M
+ */
+ public boolean postSkew(float kx, float ky) {
+ nPostSkew(native_instance, kx, ky);
+ return true;
+ }
+
+ /**
+ * Postconcats the matrix with the specified matrix. M' = other * M
+ */
+ public boolean postConcat(Matrix other) {
+ nPostConcat(native_instance, other.native_instance);
+ return true;
+ }
+
+ /**
+ * Controlls how the src rect should align into the dst rect for setRectToRect().
+ */
+ public enum ScaleToFit {
+ /**
+ * Scale in X and Y independently, so that src matches dst exactly. This may change the
+ * aspect ratio of the src.
+ */
+ FILL(0),
+ /**
+ * Compute a scale that will maintain the original src aspect ratio, but will also ensure
+ * that src fits entirely inside dst. At least one axis (X or Y) will fit exactly. START
+ * aligns the result to the left and top edges of dst.
+ */
+ START(1),
+ /**
+ * Compute a scale that will maintain the original src aspect ratio, but will also ensure
+ * that src fits entirely inside dst. At least one axis (X or Y) will fit exactly. The
+ * result is centered inside dst.
+ */
+ CENTER(2),
+ /**
+ * Compute a scale that will maintain the original src aspect ratio, but will also ensure
+ * that src fits entirely inside dst. At least one axis (X or Y) will fit exactly. END
+ * aligns the result to the right and bottom edges of dst.
+ */
+ END(3);
+
+ // the native values must match those in SkMatrix.h
+ ScaleToFit(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+
+ final int nativeInt;
+ }
+
+ /**
+ * Set the matrix to the scale and translate values that map the source rectangle to the
+ * destination rectangle, returning true if the the result can be represented.
+ *
+ * @param src the source rectangle to map from.
+ * @param dst the destination rectangle to map to.
+ * @param stf the ScaleToFit option
+ * @return true if the matrix can be represented by the rectangle mapping.
+ */
+ public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) {
+ if (dst == null || src == null) {
+ throw new NullPointerException();
+ }
+ return nSetRectToRect(native_instance, src, dst, stf.nativeInt);
+ }
+
+ // private helper to perform range checks on arrays of "points"
+ private static void checkPointArrays(float[] src, int srcIndex,
+ float[] dst, int dstIndex,
+ int pointCount) {
+ // check for too-small and too-big indices
+ int srcStop = srcIndex + (pointCount << 1);
+ int dstStop = dstIndex + (pointCount << 1);
+ if ((pointCount | srcIndex | dstIndex | srcStop | dstStop) < 0 ||
+ srcStop > src.length || dstStop > dst.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ }
+
+ /**
+ * Set the matrix such that the specified src points would map to the specified dst points. The
+ * "points" are represented as an array of floats, order [x0, y0, x1, y1, ...], where each
+ * "point" is 2 float values.
+ *
+ * @param src The array of src [x,y] pairs (points)
+ * @param srcIndex Index of the first pair of src values
+ * @param dst The array of dst [x,y] pairs (points)
+ * @param dstIndex Index of the first pair of dst values
+ * @param pointCount The number of pairs/points to be used. Must be [0..4]
+ * @return true if the matrix was set to the specified transformation
+ */
+ public boolean setPolyToPoly(float[] src, int srcIndex,
+ float[] dst, int dstIndex,
+ int pointCount) {
+ if (pointCount > 4) {
+ throw new IllegalArgumentException();
+ }
+ checkPointArrays(src, srcIndex, dst, dstIndex, pointCount);
+ return nSetPolyToPoly(native_instance, src, srcIndex,
+ dst, dstIndex, pointCount);
+ }
+
+ /**
+ * If this matrix can be inverted, return true and if inverse is not null, set inverse to be the
+ * inverse of this matrix. If this matrix cannot be inverted, ignore inverse and return false.
+ */
+ public boolean invert(Matrix inverse) {
+ return nInvert(native_instance, inverse.native_instance);
+ }
+
+ /**
+ * Apply this matrix to the array of 2D points specified by src, and write the transformed
+ * points into the array of points specified by dst. The two arrays represent their "points" as
+ * pairs of floats [x, y].
+ *
+ * @param dst The array of dst points (x,y pairs)
+ * @param dstIndex The index of the first [x,y] pair of dst floats
+ * @param src The array of src points (x,y pairs)
+ * @param srcIndex The index of the first [x,y] pair of src floats
+ * @param pointCount The number of points (x,y pairs) to transform
+ */
+ public void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex,
+ int pointCount) {
+ checkPointArrays(src, srcIndex, dst, dstIndex, pointCount);
+ nMapPoints(native_instance, dst, dstIndex, src, srcIndex,
+ pointCount, true);
+ }
+
+ /**
+ * Apply this matrix to the array of 2D vectors specified by src, and write the transformed
+ * vectors into the array of vectors specified by dst. The two arrays represent their "vectors"
+ * as pairs of floats [x, y]. Note: this method does not apply the translation associated with
+ * the matrix. Use {@link Matrix#mapPoints(float[], int, float[], int, int)} if you want the
+ * translation to be applied.
+ *
+ * @param dst The array of dst vectors (x,y pairs)
+ * @param dstIndex The index of the first [x,y] pair of dst floats
+ * @param src The array of src vectors (x,y pairs)
+ * @param srcIndex The index of the first [x,y] pair of src floats
+ * @param vectorCount The number of vectors (x,y pairs) to transform
+ */
+ public void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex,
+ int vectorCount) {
+ checkPointArrays(src, srcIndex, dst, dstIndex, vectorCount);
+ nMapPoints(native_instance, dst, dstIndex, src, srcIndex,
+ vectorCount, false);
+ }
+
+ /**
+ * Apply this matrix to the array of 2D points specified by src, and write the transformed
+ * points into the array of points specified by dst. The two arrays represent their "points" as
+ * pairs of floats [x, y].
+ *
+ * @param dst The array of dst points (x,y pairs)
+ * @param src The array of src points (x,y pairs)
+ */
+ public void mapPoints(float[] dst, float[] src) {
+ if (dst.length != src.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ mapPoints(dst, 0, src, 0, dst.length >> 1);
+ }
+
+ /**
+ * Apply this matrix to the array of 2D vectors specified by src, and write the transformed
+ * vectors into the array of vectors specified by dst. The two arrays represent their "vectors"
+ * as pairs of floats [x, y]. Note: this method does not apply the translation associated with
+ * the matrix. Use {@link Matrix#mapPoints(float[], float[])} if you want the translation to be
+ * applied.
+ *
+ * @param dst The array of dst vectors (x,y pairs)
+ * @param src The array of src vectors (x,y pairs)
+ */
+ public void mapVectors(float[] dst, float[] src) {
+ if (dst.length != src.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ mapVectors(dst, 0, src, 0, dst.length >> 1);
+ }
+
+ /**
+ * Apply this matrix to the array of 2D points, and write the transformed points back into the
+ * array
+ *
+ * @param pts The array [x0, y0, x1, y1, ...] of points to transform.
+ */
+ public void mapPoints(float[] pts) {
+ mapPoints(pts, 0, pts, 0, pts.length >> 1);
+ }
+
+ /**
+ * Apply this matrix to the array of 2D vectors, and write the transformed vectors back into the
+ * array. Note: this method does not apply the translation associated with the matrix. Use
+ * {@link Matrix#mapPoints(float[])} if you want the translation to be applied.
+ *
+ * @param vecs The array [x0, y0, x1, y1, ...] of vectors to transform.
+ */
+ public void mapVectors(float[] vecs) {
+ mapVectors(vecs, 0, vecs, 0, vecs.length >> 1);
+ }
+
+ /**
+ * Apply this matrix to the src rectangle, and write the transformed rectangle into dst. This is
+ * accomplished by transforming the 4 corners of src, and then setting dst to the bounds of
+ * those points.
+ *
+ * @param dst Where the transformed rectangle is written.
+ * @param src The original rectangle to be transformed.
+ * @return the result of calling rectStaysRect()
+ */
+ public boolean mapRect(RectF dst, RectF src) {
+ if (dst == null || src == null) {
+ throw new NullPointerException();
+ }
+ return nMapRect(native_instance, dst, src);
+ }
+
+ /**
+ * Apply this matrix to the rectangle, and write the transformed rectangle back into it. This is
+ * accomplished by transforming the 4 corners of rect, and then setting it to the bounds of
+ * those points
+ *
+ * @param rect The rectangle to transform.
+ * @return the result of calling rectStaysRect()
+ */
+ public boolean mapRect(RectF rect) {
+ return mapRect(rect, rect);
+ }
+
+ /**
+ * Return the mean radius of a circle after it has been mapped by this matrix. NOTE: in
+ * perspective this value assumes the circle has its center at the origin.
+ */
+ public float mapRadius(float radius) {
+ return nMapRadius(native_instance, radius);
+ }
+
+ /**
+ * Copy 9 values from the matrix into the array.
+ */
+ public void getValues(float[] values) {
+ if (values.length < 9) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ nGetValues(native_instance, values);
+ }
+
+ /**
+ * Copy 9 values from the array into the matrix. Depending on the implementation of Matrix,
+ * these may be transformed into 16.16 integers in the Matrix, such that a subsequent call to
+ * getValues() will not yield exactly the same values.
+ */
+ public void setValues(float[] values) {
+ if (values.length < 9) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ nSetValues(native_instance, values);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(64);
+ sb.append("Matrix{");
+ toShortString(sb);
+ sb.append('}');
+ return sb.toString();
+
+ }
+
+ public String toShortString() {
+ StringBuilder sb = new StringBuilder(64);
+ toShortString(sb);
+ return sb.toString();
+ }
+
+ /**
+ * @hide
+ */
+ public void toShortString(StringBuilder sb) {
+ float[] values = new float[9];
+ getValues(values);
+ sb.append('[');
+ sb.append(values[0]);
+ sb.append(", ");
+ sb.append(values[1]);
+ sb.append(", ");
+ sb.append(values[2]);
+ sb.append("][");
+ sb.append(values[3]);
+ sb.append(", ");
+ sb.append(values[4]);
+ sb.append(", ");
+ sb.append(values[5]);
+ sb.append("][");
+ sb.append(values[6]);
+ sb.append(", ");
+ sb.append(values[7]);
+ sb.append(", ");
+ sb.append(values[8]);
+ sb.append(']');
+ }
+
+ /**
+ * Print short string, to optimize dumping.
+ *
+ * @hide
+ */
+ public void printShortString(PrintWriter pw) {
+ float[] values = new float[9];
+ getValues(values);
+ pw.print('[');
+ pw.print(values[0]);
+ pw.print(", ");
+ pw.print(values[1]);
+ pw.print(", ");
+ pw.print(values[2]);
+ pw.print("][");
+ pw.print(values[3]);
+ pw.print(", ");
+ pw.print(values[4]);
+ pw.print(", ");
+ pw.print(values[5]);
+ pw.print("][");
+ pw.print(values[6]);
+ pw.print(", ");
+ pw.print(values[7]);
+ pw.print(", ");
+ pw.print(values[8]);
+ pw.print(']');
+
+ }
+
+ /** @hide */
+ public final long ni() {
+ return native_instance;
+ }
+
+ // ------------------ Regular JNI ------------------------
+
+ private static native long nCreate(long nSrc_or_zero);
+ private static native long nGetNativeFinalizer();
+
+
+ // ------------------ Fast JNI ------------------------
+
+ @FastNative
+ private static native boolean nSetRectToRect(long nObject,
+ RectF src, RectF dst, int stf);
+ @FastNative
+ private static native boolean nSetPolyToPoly(long nObject,
+ float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount);
+ @FastNative
+ private static native void nMapPoints(long nObject,
+ float[] dst, int dstIndex, float[] src, int srcIndex,
+ int ptCount, boolean isPts);
+ @FastNative
+ private static native boolean nMapRect(long nObject, RectF dst, RectF src);
+ @FastNative
+ private static native void nGetValues(long nObject, float[] values);
+ @FastNative
+ private static native void nSetValues(long nObject, float[] values);
+
+
+ // ------------------ Critical JNI ------------------------
+
+ @CriticalNative
+ private static native boolean nIsIdentity(long nObject);
+ @CriticalNative
+ private static native boolean nIsAffine(long nObject);
+ @CriticalNative
+ private static native boolean nRectStaysRect(long nObject);
+ @CriticalNative
+ private static native void nReset(long nObject);
+ @CriticalNative
+ private static native void nSet(long nObject, long nOther);
+ @CriticalNative
+ private static native void nSetTranslate(long nObject, float dx, float dy);
+ @CriticalNative
+ private static native void nSetScale(long nObject, float sx, float sy, float px, float py);
+ @CriticalNative
+ private static native void nSetScale(long nObject, float sx, float sy);
+ @CriticalNative
+ private static native void nSetRotate(long nObject, float degrees, float px, float py);
+ @CriticalNative
+ private static native void nSetRotate(long nObject, float degrees);
+ @CriticalNative
+ private static native void nSetSinCos(long nObject, float sinValue, float cosValue,
+ float px, float py);
+ @CriticalNative
+ private static native void nSetSinCos(long nObject, float sinValue, float cosValue);
+ @CriticalNative
+ private static native void nSetSkew(long nObject, float kx, float ky, float px, float py);
+ @CriticalNative
+ private static native void nSetSkew(long nObject, float kx, float ky);
+ @CriticalNative
+ private static native void nSetConcat(long nObject, long nA, long nB);
+ @CriticalNative
+ private static native void nPreTranslate(long nObject, float dx, float dy);
+ @CriticalNative
+ private static native void nPreScale(long nObject, float sx, float sy, float px, float py);
+ @CriticalNative
+ private static native void nPreScale(long nObject, float sx, float sy);
+ @CriticalNative
+ private static native void nPreRotate(long nObject, float degrees, float px, float py);
+ @CriticalNative
+ private static native void nPreRotate(long nObject, float degrees);
+ @CriticalNative
+ private static native void nPreSkew(long nObject, float kx, float ky, float px, float py);
+ @CriticalNative
+ private static native void nPreSkew(long nObject, float kx, float ky);
+ @CriticalNative
+ private static native void nPreConcat(long nObject, long nOther_matrix);
+ @CriticalNative
+ private static native void nPostTranslate(long nObject, float dx, float dy);
+ @CriticalNative
+ private static native void nPostScale(long nObject, float sx, float sy, float px, float py);
+ @CriticalNative
+ private static native void nPostScale(long nObject, float sx, float sy);
+ @CriticalNative
+ private static native void nPostRotate(long nObject, float degrees, float px, float py);
+ @CriticalNative
+ private static native void nPostRotate(long nObject, float degrees);
+ @CriticalNative
+ private static native void nPostSkew(long nObject, float kx, float ky, float px, float py);
+ @CriticalNative
+ private static native void nPostSkew(long nObject, float kx, float ky);
+ @CriticalNative
+ private static native void nPostConcat(long nObject, long nOther_matrix);
+ @CriticalNative
+ private static native boolean nInvert(long nObject, long nInverse);
+ @CriticalNative
+ private static native float nMapRadius(long nObject, float radius);
+ @CriticalNative
+ private static native boolean nEquals(long nA, long nB);
+}
diff --git a/android/graphics/Matrix_Delegate.java b/android/graphics/Matrix_Delegate.java
new file mode 100644
index 0000000..354f919
--- /dev/null
+++ b/android/graphics/Matrix_Delegate.java
@@ -0,0 +1,1077 @@
+/*
+ * 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 android.graphics;
+
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.graphics.Matrix.ScaleToFit;
+
+import java.awt.geom.AffineTransform;
+import java.awt.geom.NoninvertibleTransformException;
+
+import libcore.util.NativeAllocationRegistry_Delegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.Matrix
+ *
+ * Through the layoutlib_create tool, the original native methods of Matrix have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original Matrix class.
+ *
+ * @see DelegateManager
+ *
+ */
+public final class Matrix_Delegate {
+
+ private final static int MATRIX_SIZE = 9;
+
+ // ---- delegate manager ----
+ private static final DelegateManager<Matrix_Delegate> sManager =
+ new DelegateManager<Matrix_Delegate>(Matrix_Delegate.class);
+ private static long sFinalizer = -1;
+
+ // ---- delegate data ----
+ private float mValues[] = new float[MATRIX_SIZE];
+
+ // ---- Public Helper methods ----
+
+ public static Matrix_Delegate getDelegate(long native_instance) {
+ return sManager.getDelegate(native_instance);
+ }
+
+ /**
+ * Returns an {@link AffineTransform} matching the given Matrix.
+ */
+ public static AffineTransform getAffineTransform(Matrix m) {
+ Matrix_Delegate delegate = sManager.getDelegate(m.native_instance);
+ if (delegate == null) {
+ return null;
+ }
+
+ return delegate.getAffineTransform();
+ }
+
+ public static boolean hasPerspective(Matrix m) {
+ Matrix_Delegate delegate = sManager.getDelegate(m.native_instance);
+ if (delegate == null) {
+ return false;
+ }
+
+ return delegate.hasPerspective();
+ }
+
+ /**
+ * Sets the content of the matrix with the content of another matrix.
+ */
+ public void set(Matrix_Delegate matrix) {
+ System.arraycopy(matrix.mValues, 0, mValues, 0, MATRIX_SIZE);
+ }
+
+ /**
+ * Sets the content of the matrix with the content of another matrix represented as an array
+ * of values.
+ */
+ public void set(float[] values) {
+ System.arraycopy(values, 0, mValues, 0, MATRIX_SIZE);
+ }
+
+ /**
+ * Resets the matrix to be the identity matrix.
+ */
+ public void reset() {
+ reset(mValues);
+ }
+
+ /**
+ * Returns whether or not the matrix is identity.
+ */
+ public boolean isIdentity() {
+ for (int i = 0, k = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++, k++) {
+ if (mValues[k] != ((i==j) ? 1 : 0)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public static float[] makeValues(AffineTransform matrix) {
+ float[] values = new float[MATRIX_SIZE];
+ values[0] = (float) matrix.getScaleX();
+ values[1] = (float) matrix.getShearX();
+ values[2] = (float) matrix.getTranslateX();
+ values[3] = (float) matrix.getShearY();
+ values[4] = (float) matrix.getScaleY();
+ values[5] = (float) matrix.getTranslateY();
+ values[6] = 0.f;
+ values[7] = 0.f;
+ values[8] = 1.f;
+
+ return values;
+ }
+
+ public static Matrix_Delegate make(AffineTransform matrix) {
+ return new Matrix_Delegate(makeValues(matrix));
+ }
+
+ public boolean mapRect(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.
+ mapPoints(corners);
+
+ // 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]));
+
+
+ return (computeTypeMask() & kRectStaysRect_Mask) != 0;
+ }
+
+
+ /**
+ * Returns an {@link AffineTransform} matching the matrix.
+ */
+ public AffineTransform getAffineTransform() {
+ return getAffineTransform(mValues);
+ }
+
+ public boolean hasPerspective() {
+ return (mValues[6] != 0 || mValues[7] != 0 || mValues[8] != 1);
+ }
+
+
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreate(long native_src_or_zero) {
+ // create the delegate
+ Matrix_Delegate newDelegate = new Matrix_Delegate();
+
+ // copy from values if needed.
+ if (native_src_or_zero > 0) {
+ Matrix_Delegate oldDelegate = sManager.getDelegate(native_src_or_zero);
+ if (oldDelegate != null) {
+ System.arraycopy(
+ oldDelegate.mValues, 0,
+ newDelegate.mValues, 0,
+ MATRIX_SIZE);
+ }
+ }
+
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nIsIdentity(long native_object) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return false;
+ }
+
+ return d.isIdentity();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nIsAffine(long native_object) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return true;
+ }
+
+ return (d.computeTypeMask() & kPerspective_Mask) == 0;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nRectStaysRect(long native_object) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return true;
+ }
+
+ return (d.computeTypeMask() & kRectStaysRect_Mask) != 0;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nReset(long native_object) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ reset(d.mValues);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSet(long native_object, long other) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ Matrix_Delegate src = sManager.getDelegate(other);
+ if (src == null) {
+ return;
+ }
+
+ System.arraycopy(src.mValues, 0, d.mValues, 0, MATRIX_SIZE);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetTranslate(long native_object, float dx, float dy) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ setTranslate(d.mValues, dx, dy);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetScale(long native_object, float sx, float sy,
+ float px, float py) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ d.mValues = getScale(sx, sy, px, py);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetScale(long native_object, float sx, float sy) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ d.mValues[0] = sx;
+ d.mValues[1] = 0;
+ d.mValues[2] = 0;
+ d.mValues[3] = 0;
+ d.mValues[4] = sy;
+ d.mValues[5] = 0;
+ d.mValues[6] = 0;
+ d.mValues[7] = 0;
+ d.mValues[8] = 1;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetRotate(long native_object, float degrees, float px, float py) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ d.mValues = getRotate(degrees, px, py);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetRotate(long native_object, float degrees) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ setRotate(d.mValues, degrees);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetSinCos(long native_object, float sinValue, float cosValue,
+ float px, float py) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ // TODO: do it in one pass
+
+ // translate so that the pivot is in 0,0
+ setTranslate(d.mValues, -px, -py);
+
+ // scale
+ d.postTransform(getRotate(sinValue, cosValue));
+ // translate back the pivot
+ d.postTransform(getTranslate(px, py));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetSinCos(long native_object, float sinValue, float cosValue) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ setRotate(d.mValues, sinValue, cosValue);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetSkew(long native_object, float kx, float ky,
+ float px, float py) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ d.mValues = getSkew(kx, ky, px, py);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetSkew(long native_object, float kx, float ky) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ d.mValues[0] = 1;
+ d.mValues[1] = kx;
+ d.mValues[2] = -0;
+ d.mValues[3] = ky;
+ d.mValues[4] = 1;
+ d.mValues[5] = 0;
+ d.mValues[6] = 0;
+ d.mValues[7] = 0;
+ d.mValues[8] = 1;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetConcat(long native_object, long a, long b) {
+ if (a == native_object) {
+ nPreConcat(native_object, b);
+ return;
+ } else if (b == native_object) {
+ nPostConcat(native_object, a);
+ return;
+ }
+
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ Matrix_Delegate a_mtx = sManager.getDelegate(a);
+ Matrix_Delegate b_mtx = sManager.getDelegate(b);
+ if (d != null && a_mtx != null && b_mtx != null) {
+ multiply(d.mValues, a_mtx.mValues, b_mtx.mValues);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPreTranslate(long native_object, float dx, float dy) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.preTransform(getTranslate(dx, dy));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPreScale(long native_object, float sx, float sy,
+ float px, float py) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.preTransform(getScale(sx, sy, px, py));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPreScale(long native_object, float sx, float sy) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.preTransform(getScale(sx, sy));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPreRotate(long native_object, float degrees,
+ float px, float py) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.preTransform(getRotate(degrees, px, py));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPreRotate(long native_object, float degrees) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+
+ double rad = Math.toRadians(degrees);
+ float sin = (float) Math.sin(rad);
+ float cos = (float) Math.cos(rad);
+
+ d.preTransform(getRotate(sin, cos));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPreSkew(long native_object, float kx, float ky,
+ float px, float py) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.preTransform(getSkew(kx, ky, px, py));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPreSkew(long native_object, float kx, float ky) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.preTransform(getSkew(kx, ky));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPreConcat(long native_object, long other_matrix) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ Matrix_Delegate other = sManager.getDelegate(other_matrix);
+ if (d != null && other != null) {
+ d.preTransform(other.mValues);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPostTranslate(long native_object, float dx, float dy) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.postTransform(getTranslate(dx, dy));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPostScale(long native_object, float sx, float sy,
+ float px, float py) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.postTransform(getScale(sx, sy, px, py));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPostScale(long native_object, float sx, float sy) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.postTransform(getScale(sx, sy));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPostRotate(long native_object, float degrees,
+ float px, float py) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.postTransform(getRotate(degrees, px, py));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPostRotate(long native_object, float degrees) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.postTransform(getRotate(degrees));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPostSkew(long native_object, float kx, float ky,
+ float px, float py) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.postTransform(getSkew(kx, ky, px, py));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPostSkew(long native_object, float kx, float ky) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d != null) {
+ d.postTransform(getSkew(kx, ky));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nPostConcat(long native_object, long other_matrix) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ Matrix_Delegate other = sManager.getDelegate(other_matrix);
+ if (d != null && other != null) {
+ d.postTransform(other.mValues);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nSetRectToRect(long native_object, RectF src,
+ RectF dst, int stf) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return false;
+ }
+
+ if (src.isEmpty()) {
+ reset(d.mValues);
+ return false;
+ }
+
+ if (dst.isEmpty()) {
+ d.mValues[0] = d.mValues[1] = d.mValues[2] = d.mValues[3] = d.mValues[4] = d.mValues[5]
+ = d.mValues[6] = d.mValues[7] = 0;
+ d.mValues[8] = 1;
+ } else {
+ float tx, sx = dst.width() / src.width();
+ float ty, sy = dst.height() / src.height();
+ boolean xLarger = false;
+
+ if (stf != ScaleToFit.FILL.nativeInt) {
+ if (sx > sy) {
+ xLarger = true;
+ sx = sy;
+ } else {
+ sy = sx;
+ }
+ }
+
+ tx = dst.left - src.left * sx;
+ ty = dst.top - src.top * sy;
+ if (stf == ScaleToFit.CENTER.nativeInt || stf == ScaleToFit.END.nativeInt) {
+ float diff;
+
+ if (xLarger) {
+ diff = dst.width() - src.width() * sy;
+ } else {
+ diff = dst.height() - src.height() * sy;
+ }
+
+ if (stf == ScaleToFit.CENTER.nativeInt) {
+ diff = diff / 2;
+ }
+
+ if (xLarger) {
+ tx += diff;
+ } else {
+ ty += diff;
+ }
+ }
+
+ d.mValues[0] = sx;
+ d.mValues[4] = sy;
+ d.mValues[2] = tx;
+ d.mValues[5] = ty;
+ d.mValues[1] = d.mValues[3] = d.mValues[6] = d.mValues[7] = 0;
+
+ }
+ // shared cleanup
+ d.mValues[8] = 1;
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nSetPolyToPoly(long native_object, float[] src, int srcIndex,
+ float[] dst, int dstIndex, int pointCount) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Matrix.setPolyToPoly is not supported.",
+ null, null /*data*/);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nInvert(long native_object, long inverse) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return false;
+ }
+
+ Matrix_Delegate inv_mtx = sManager.getDelegate(inverse);
+ if (inv_mtx == null) {
+ return false;
+ }
+
+ try {
+ AffineTransform affineTransform = d.getAffineTransform();
+ AffineTransform inverseTransform = affineTransform.createInverse();
+ inv_mtx.mValues[0] = (float)inverseTransform.getScaleX();
+ inv_mtx.mValues[1] = (float)inverseTransform.getShearX();
+ inv_mtx.mValues[2] = (float)inverseTransform.getTranslateX();
+ inv_mtx.mValues[3] = (float)inverseTransform.getScaleX();
+ inv_mtx.mValues[4] = (float)inverseTransform.getShearY();
+ inv_mtx.mValues[5] = (float)inverseTransform.getTranslateY();
+
+ return true;
+ } catch (NoninvertibleTransformException e) {
+ return false;
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nMapPoints(long native_object, float[] dst, int dstIndex,
+ float[] src, int srcIndex, int ptCount, boolean isPts) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ if (isPts) {
+ d.mapPoints(dst, dstIndex, src, srcIndex, ptCount);
+ } else {
+ d.mapVectors(dst, dstIndex, src, srcIndex, ptCount);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nMapRect(long native_object, RectF dst, RectF src) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return false;
+ }
+
+ return d.mapRect(dst, src);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nMapRadius(long native_object, float radius) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return 0.f;
+ }
+
+ float[] src = new float[] { radius, 0.f, 0.f, radius };
+ d.mapVectors(src, 0, src, 0, 2);
+
+ float l1 = (float) Math.hypot(src[0], src[1]);
+ float l2 = (float) Math.hypot(src[2], src[3]);
+ return (float) Math.sqrt(l1 * l2);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nGetValues(long native_object, float[] values) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ System.arraycopy(d.mValues, 0, values, 0, MATRIX_SIZE);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetValues(long native_object, float[] values) {
+ Matrix_Delegate d = sManager.getDelegate(native_object);
+ if (d == null) {
+ return;
+ }
+
+ System.arraycopy(values, 0, d.mValues, 0, MATRIX_SIZE);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nEquals(long native_a, long native_b) {
+ Matrix_Delegate a = sManager.getDelegate(native_a);
+ if (a == null) {
+ return false;
+ }
+
+ Matrix_Delegate b = sManager.getDelegate(native_b);
+ if (b == null) {
+ return false;
+ }
+
+ for (int i = 0 ; i < MATRIX_SIZE ; i++) {
+ if (a.mValues[i] != b.mValues[i]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nGetNativeFinalizer() {
+ synchronized (Matrix_Delegate.class) {
+ if (sFinalizer == -1) {
+ sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(sManager::removeJavaReferenceFor);
+ }
+ }
+ return sFinalizer;
+ }
+
+ // ---- Private helper methods ----
+
+ /*package*/ static AffineTransform getAffineTransform(float[] matrix) {
+ // the AffineTransform constructor takes the value in a different order
+ // for a matrix [ 0 1 2 ]
+ // [ 3 4 5 ]
+ // the order is 0, 3, 1, 4, 2, 5...
+ return new AffineTransform(
+ matrix[0], matrix[3], matrix[1],
+ matrix[4], matrix[2], matrix[5]);
+ }
+
+ /**
+ * Reset a matrix to the identity
+ */
+ private static void reset(float[] mtx) {
+ for (int i = 0, k = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++, k++) {
+ mtx[k] = ((i==j) ? 1 : 0);
+ }
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private final static int kIdentity_Mask = 0;
+ private final static int kTranslate_Mask = 0x01; //!< set if the matrix has translation
+ private final static int kScale_Mask = 0x02; //!< set if the matrix has X or Y scale
+ private final static int kAffine_Mask = 0x04; //!< set if the matrix skews or rotates
+ private final static int kPerspective_Mask = 0x08; //!< set if the matrix is in perspective
+ private final static int kRectStaysRect_Mask = 0x10;
+ @SuppressWarnings("unused")
+ private final static int kUnknown_Mask = 0x80;
+
+ @SuppressWarnings("unused")
+ private final static int kAllMasks = kTranslate_Mask |
+ kScale_Mask |
+ kAffine_Mask |
+ kPerspective_Mask |
+ kRectStaysRect_Mask;
+
+ // these guys align with the masks, so we can compute a mask from a variable 0/1
+ @SuppressWarnings("unused")
+ private final static int kTranslate_Shift = 0;
+ @SuppressWarnings("unused")
+ private final static int kScale_Shift = 1;
+ @SuppressWarnings("unused")
+ private final static int kAffine_Shift = 2;
+ @SuppressWarnings("unused")
+ private final static int kPerspective_Shift = 3;
+ private final static int kRectStaysRect_Shift = 4;
+
+ private int computeTypeMask() {
+ int mask = 0;
+
+ if (mValues[6] != 0. || mValues[7] != 0. || mValues[8] != 1.) {
+ mask |= kPerspective_Mask;
+ }
+
+ if (mValues[2] != 0. || mValues[5] != 0.) {
+ mask |= kTranslate_Mask;
+ }
+
+ float m00 = mValues[0];
+ float m01 = mValues[1];
+ float m10 = mValues[3];
+ float m11 = mValues[4];
+
+ if (m01 != 0. || m10 != 0.) {
+ mask |= kAffine_Mask;
+ }
+
+ if (m00 != 1. || m11 != 1.) {
+ mask |= kScale_Mask;
+ }
+
+ if ((mask & kPerspective_Mask) == 0) {
+ // map non-zero to 1
+ int im00 = m00 != 0 ? 1 : 0;
+ int im01 = m01 != 0 ? 1 : 0;
+ int im10 = m10 != 0 ? 1 : 0;
+ int im11 = m11 != 0 ? 1 : 0;
+
+ // record if the (p)rimary and (s)econdary diagonals are all 0 or
+ // all non-zero (answer is 0 or 1)
+ int dp0 = (im00 | im11) ^ 1; // true if both are 0
+ int dp1 = im00 & im11; // true if both are 1
+ int ds0 = (im01 | im10) ^ 1; // true if both are 0
+ int ds1 = im01 & im10; // true if both are 1
+
+ // return 1 if primary is 1 and secondary is 0 or
+ // primary is 0 and secondary is 1
+ mask |= ((dp0 & ds1) | (dp1 & ds0)) << kRectStaysRect_Shift;
+ }
+
+ return mask;
+ }
+
+ private Matrix_Delegate() {
+ reset();
+ }
+
+ private Matrix_Delegate(float[] values) {
+ System.arraycopy(values, 0, mValues, 0, MATRIX_SIZE);
+ }
+
+ /**
+ * Adds the given transformation to the current Matrix
+ * <p/>This in effect does this = this*matrix
+ * @param matrix
+ */
+ private void postTransform(float[] matrix) {
+ float[] tmp = new float[9];
+ multiply(tmp, mValues, matrix);
+ mValues = tmp;
+ }
+
+ /**
+ * Adds the given transformation to the current Matrix
+ * <p/>This in effect does this = matrix*this
+ * @param matrix
+ */
+ private void preTransform(float[] matrix) {
+ float[] tmp = new float[9];
+ multiply(tmp, matrix, mValues);
+ mValues = tmp;
+ }
+
+ /**
+ * Apply this matrix to the array of 2D points specified by src, and write
+ * the transformed points into the array of points specified by dst. The
+ * two arrays represent their "points" as pairs of floats [x, y].
+ *
+ * @param dst The array of dst points (x,y pairs)
+ * @param dstIndex The index of the first [x,y] pair of dst floats
+ * @param src The array of src points (x,y pairs)
+ * @param srcIndex The index of the first [x,y] pair of src floats
+ * @param pointCount The number of points (x,y pairs) to transform
+ */
+
+ private void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex,
+ int pointCount) {
+ final int count = pointCount * 2;
+
+ float[] tmpDest = dst;
+ boolean inPlace = dst == src;
+ if (inPlace) {
+ tmpDest = new float[dstIndex + count];
+ }
+
+ for (int i = 0 ; i < count ; i += 2) {
+ // just in case we are doing in place, we better put this in temp vars
+ float x = mValues[0] * src[i + srcIndex] +
+ mValues[1] * src[i + srcIndex + 1] +
+ mValues[2];
+ float y = mValues[3] * src[i + srcIndex] +
+ mValues[4] * src[i + srcIndex + 1] +
+ mValues[5];
+
+ tmpDest[i + dstIndex] = x;
+ tmpDest[i + dstIndex + 1] = y;
+ }
+
+ if (inPlace) {
+ System.arraycopy(tmpDest, dstIndex, dst, dstIndex, count);
+ }
+ }
+
+ /**
+ * Apply this matrix to the array of 2D points, and write the transformed
+ * points back into the array
+ *
+ * @param pts The array [x0, y0, x1, y1, ...] of points to transform.
+ */
+
+ private void mapPoints(float[] pts) {
+ mapPoints(pts, 0, pts, 0, pts.length >> 1);
+ }
+
+ private void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex, int ptCount) {
+ if (hasPerspective()) {
+ // transform the (0,0) point
+ float[] origin = new float[] { 0.f, 0.f};
+ mapPoints(origin);
+
+ // translate the vector data as points
+ mapPoints(dst, dstIndex, src, srcIndex, ptCount);
+
+ // then substract the transformed origin.
+ final int count = ptCount * 2;
+ for (int i = 0 ; i < count ; i += 2) {
+ dst[dstIndex + i] = dst[dstIndex + i] - origin[0];
+ dst[dstIndex + i + 1] = dst[dstIndex + i + 1] - origin[1];
+ }
+ } else {
+ // make a copy of the matrix
+ Matrix_Delegate copy = new Matrix_Delegate(mValues);
+
+ // remove the translation
+ setTranslate(copy.mValues, 0, 0);
+
+ // map the content as points.
+ copy.mapPoints(dst, dstIndex, src, srcIndex, ptCount);
+ }
+ }
+
+ /**
+ * multiply two matrices and store them in a 3rd.
+ * <p/>This in effect does dest = a*b
+ * dest cannot be the same as a or b.
+ */
+ /*package*/ static void multiply(float dest[], float[] a, float[] b) {
+ // first row
+ dest[0] = b[0] * a[0] + b[1] * a[3] + b[2] * a[6];
+ dest[1] = b[0] * a[1] + b[1] * a[4] + b[2] * a[7];
+ dest[2] = b[0] * a[2] + b[1] * a[5] + b[2] * a[8];
+
+ // 2nd row
+ dest[3] = b[3] * a[0] + b[4] * a[3] + b[5] * a[6];
+ dest[4] = b[3] * a[1] + b[4] * a[4] + b[5] * a[7];
+ dest[5] = b[3] * a[2] + b[4] * a[5] + b[5] * a[8];
+
+ // 3rd row
+ dest[6] = b[6] * a[0] + b[7] * a[3] + b[8] * a[6];
+ dest[7] = b[6] * a[1] + b[7] * a[4] + b[8] * a[7];
+ dest[8] = b[6] * a[2] + b[7] * a[5] + b[8] * a[8];
+ }
+
+ /**
+ * Returns a matrix that represents a given translate
+ * @param dx
+ * @param dy
+ * @return
+ */
+ /*package*/ static float[] getTranslate(float dx, float dy) {
+ return setTranslate(new float[9], dx, dy);
+ }
+
+ /*package*/ static float[] setTranslate(float[] dest, float dx, float dy) {
+ dest[0] = 1;
+ dest[1] = 0;
+ dest[2] = dx;
+ dest[3] = 0;
+ dest[4] = 1;
+ dest[5] = dy;
+ dest[6] = 0;
+ dest[7] = 0;
+ dest[8] = 1;
+ return dest;
+ }
+
+ /*package*/ static float[] getScale(float sx, float sy) {
+ return new float[] { sx, 0, 0, 0, sy, 0, 0, 0, 1 };
+ }
+
+ /**
+ * Returns a matrix that represents the given scale info.
+ * @param sx
+ * @param sy
+ * @param px
+ * @param py
+ */
+ /*package*/ static float[] getScale(float sx, float sy, float px, float py) {
+ float[] tmp = new float[9];
+ float[] tmp2 = new float[9];
+
+ // TODO: do it in one pass
+
+ // translate tmp so that the pivot is in 0,0
+ setTranslate(tmp, -px, -py);
+
+ // scale into tmp2
+ multiply(tmp2, tmp, getScale(sx, sy));
+
+ // translate back the pivot back into tmp
+ multiply(tmp, tmp2, getTranslate(px, py));
+
+ return tmp;
+ }
+
+
+ /*package*/ static float[] getRotate(float degrees) {
+ double rad = Math.toRadians(degrees);
+ float sin = (float)Math.sin(rad);
+ float cos = (float)Math.cos(rad);
+
+ return getRotate(sin, cos);
+ }
+
+ /*package*/ static float[] getRotate(float sin, float cos) {
+ return setRotate(new float[9], sin, cos);
+ }
+
+ /*package*/ static float[] setRotate(float[] dest, float degrees) {
+ double rad = Math.toRadians(degrees);
+ float sin = (float)Math.sin(rad);
+ float cos = (float)Math.cos(rad);
+
+ return setRotate(dest, sin, cos);
+ }
+
+ /*package*/ static float[] setRotate(float[] dest, float sin, float cos) {
+ dest[0] = cos;
+ dest[1] = -sin;
+ dest[2] = 0;
+ dest[3] = sin;
+ dest[4] = cos;
+ dest[5] = 0;
+ dest[6] = 0;
+ dest[7] = 0;
+ dest[8] = 1;
+ return dest;
+ }
+
+ /*package*/ static float[] getRotate(float degrees, float px, float py) {
+ float[] tmp = new float[9];
+ float[] tmp2 = new float[9];
+
+ // TODO: do it in one pass
+
+ // translate so that the pivot is in 0,0
+ setTranslate(tmp, -px, -py);
+
+ // rotate into tmp2
+ double rad = Math.toRadians(degrees);
+ float cos = (float)Math.cos(rad);
+ float sin = (float)Math.sin(rad);
+ multiply(tmp2, tmp, getRotate(sin, cos));
+
+ // translate back the pivot back into tmp
+ multiply(tmp, tmp2, getTranslate(px, py));
+
+ return tmp;
+ }
+
+ /*package*/ static float[] getSkew(float kx, float ky) {
+ return new float[] { 1, kx, 0, ky, 1, 0, 0, 0, 1 };
+ }
+
+ /*package*/ static float[] getSkew(float kx, float ky, float px, float py) {
+ float[] tmp = new float[9];
+ float[] tmp2 = new float[9];
+
+ // TODO: do it in one pass
+
+ // translate so that the pivot is in 0,0
+ setTranslate(tmp, -px, -py);
+
+ // skew into tmp2
+ multiply(tmp2, tmp, new float[] { 1, kx, 0, ky, 1, 0, 0, 0, 1 });
+ // translate back the pivot back into tmp
+ multiply(tmp, tmp2, getTranslate(px, py));
+
+ return tmp;
+ }
+}
diff --git a/android/graphics/Movie.java b/android/graphics/Movie.java
new file mode 100644
index 0000000..c8f86c6
--- /dev/null
+++ b/android/graphics/Movie.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.content.res.AssetManager;
+import java.io.InputStream;
+import java.io.FileInputStream;
+
+public class Movie {
+ private long mNativeMovie;
+
+ private Movie(long nativeMovie) {
+ if (nativeMovie == 0) {
+ throw new RuntimeException("native movie creation failed");
+ }
+ mNativeMovie = nativeMovie;
+ }
+
+ public native int width();
+ public native int height();
+ public native boolean isOpaque();
+ public native int duration();
+
+ public native boolean setTime(int relativeMilliseconds);
+
+ private native void nDraw(long nativeCanvas, float x, float y, long paintHandle);
+
+ public void draw(Canvas canvas, float x, float y, Paint paint) {
+ nDraw(canvas.getNativeCanvasWrapper(), x, y,
+ paint != null ? paint.getNativeInstance() : 0);
+ }
+
+ public void draw(Canvas canvas, float x, float y) {
+ nDraw(canvas.getNativeCanvasWrapper(), x, y, 0);
+ }
+
+ public static Movie decodeStream(InputStream is) {
+ if (is == null) {
+ return null;
+ }
+ if (is instanceof AssetManager.AssetInputStream) {
+ final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
+ return nativeDecodeAsset(asset);
+ }
+
+ return nativeDecodeStream(is);
+ }
+
+ private static native Movie nativeDecodeAsset(long asset);
+ private static native Movie nativeDecodeStream(InputStream is);
+ public static native Movie decodeByteArray(byte[] data, int offset,
+ int length);
+
+ private static native void nativeDestructor(long nativeMovie);
+
+ public static Movie decodeFile(String pathName) {
+ InputStream is;
+ try {
+ is = new FileInputStream(pathName);
+ }
+ catch (java.io.FileNotFoundException e) {
+ return null;
+ }
+ return decodeTempStream(is);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ nativeDestructor(mNativeMovie);
+ mNativeMovie = 0;
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private static Movie decodeTempStream(InputStream is) {
+ Movie moov = null;
+ try {
+ moov = decodeStream(is);
+ is.close();
+ }
+ catch (java.io.IOException e) {
+ /* do nothing.
+ If the exception happened on open, moov will be null.
+ If it happened on close, moov is still valid.
+ */
+ }
+ return moov;
+ }
+}
diff --git a/android/graphics/NinePatch.java b/android/graphics/NinePatch.java
new file mode 100644
index 0000000..b6a209f
--- /dev/null
+++ b/android/graphics/NinePatch.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * The NinePatch class permits drawing a bitmap in nine or more sections.
+ * Essentially, it allows the creation of custom graphics that will scale the
+ * way that you define, when content added within the image exceeds the normal
+ * bounds of the graphic. For a thorough explanation of a NinePatch image,
+ * read the discussion in the
+ * <a href="{@docRoot}guide/topics/graphics/2d-graphics.html#nine-patch">2D
+ * Graphics</a> document.
+ * <p>
+ * The <a href="{@docRoot}guide/developing/tools/draw9patch.html">Draw 9-Patch</a>
+ * tool offers an extremely handy way to create your NinePatch images,
+ * using a WYSIWYG graphics editor.
+ * </p>
+ */
+public class NinePatch {
+ /**
+ * Struct of inset information attached to a 9 patch bitmap.
+ *
+ * Present on a 9 patch bitmap if it optical insets were manually included,
+ * or if outline insets were automatically included by aapt.
+ *
+ * @hide
+ */
+ public static class InsetStruct {
+ @SuppressWarnings({"UnusedDeclaration"}) // called from JNI
+ InsetStruct(int opticalLeft, int opticalTop, int opticalRight, int opticalBottom,
+ int outlineLeft, int outlineTop, int outlineRight, int outlineBottom,
+ float outlineRadius, int outlineAlpha, float decodeScale) {
+ opticalRect = new Rect(opticalLeft, opticalTop, opticalRight, opticalBottom);
+ opticalRect.scale(decodeScale);
+
+ outlineRect = scaleInsets(outlineLeft, outlineTop,
+ outlineRight, outlineBottom, decodeScale);
+
+ this.outlineRadius = outlineRadius * decodeScale;
+ this.outlineAlpha = outlineAlpha / 255.0f;
+ }
+
+ public final Rect opticalRect;
+ public final Rect outlineRect;
+ public final float outlineRadius;
+ public final float outlineAlpha;
+
+ /**
+ * Scales up the rect by the given scale, ceiling values, so actual outline Rect
+ * grows toward the inside.
+ */
+ public static Rect scaleInsets(int left, int top, int right, int bottom, float scale) {
+ if (scale == 1.0f) {
+ return new Rect(left, top, right, bottom);
+ }
+
+ Rect result = new Rect();
+ result.left = (int) Math.ceil(left * scale);
+ result.top = (int) Math.ceil(top * scale);
+ result.right = (int) Math.ceil(right * scale);
+ result.bottom = (int) Math.ceil(bottom * scale);
+ return result;
+ }
+ }
+
+ private final Bitmap mBitmap;
+
+ /**
+ * Used by native code. This pointer is an instance of Res_png_9patch*.
+ *
+ * @hide
+ */
+ public long mNativeChunk;
+
+ private Paint mPaint;
+ private String mSrcName;
+
+ /**
+ * Create a drawable projection from a bitmap to nine patches.
+ *
+ * @param bitmap The bitmap describing the patches.
+ * @param chunk The 9-patch data chunk describing how the underlying bitmap
+ * is split apart and drawn.
+ */
+ public NinePatch(Bitmap bitmap, byte[] chunk) {
+ this(bitmap, chunk, null);
+ }
+
+ /**
+ * Create a drawable projection from a bitmap to nine patches.
+ *
+ * @param bitmap The bitmap describing the patches.
+ * @param chunk The 9-patch data chunk describing how the underlying
+ * bitmap is split apart and drawn.
+ * @param srcName The name of the source for the bitmap. Might be null.
+ */
+ public NinePatch(Bitmap bitmap, byte[] chunk, String srcName) {
+ mBitmap = bitmap;
+ mSrcName = srcName;
+ mNativeChunk = validateNinePatchChunk(chunk);
+ }
+
+ /**
+ * @hide
+ */
+ public NinePatch(NinePatch patch) {
+ mBitmap = patch.mBitmap;
+ mSrcName = patch.mSrcName;
+ if (patch.mPaint != null) {
+ mPaint = new Paint(patch.mPaint);
+ }
+ // No need to validate the 9patch chunk again, it was done by
+ // the instance we're copying from
+ mNativeChunk = patch.mNativeChunk;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mNativeChunk != 0) {
+ // only attempt to destroy correctly initilized chunks
+ nativeFinalize(mNativeChunk);
+ mNativeChunk = 0;
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Returns the name of this NinePatch object if one was specified
+ * when calling the constructor.
+ */
+ public String getName() {
+ return mSrcName;
+ }
+
+ /**
+ * Returns the paint used to draw this NinePatch. The paint can be null.
+ *
+ * @see #setPaint(Paint)
+ * @see #draw(Canvas, Rect)
+ * @see #draw(Canvas, RectF)
+ */
+ public Paint getPaint() {
+ return mPaint;
+ }
+
+ /**
+ * Sets the paint to use when drawing the NinePatch.
+ *
+ * @param p The paint that will be used to draw this NinePatch.
+ *
+ * @see #getPaint()
+ * @see #draw(Canvas, Rect)
+ * @see #draw(Canvas, RectF)
+ */
+ public void setPaint(Paint p) {
+ mPaint = p;
+ }
+
+ /**
+ * Returns the bitmap used to draw this NinePatch.
+ */
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ /**
+ * Draws the NinePatch. This method will use the paint returned by {@link #getPaint()}.
+ *
+ * @param canvas A container for the current matrix and clip used to draw the NinePatch.
+ * @param location Where to draw the NinePatch.
+ */
+ public void draw(Canvas canvas, RectF location) {
+ canvas.drawPatch(this, location, mPaint);
+ }
+
+ /**
+ * Draws the NinePatch. This method will use the paint returned by {@link #getPaint()}.
+ *
+ * @param canvas A container for the current matrix and clip used to draw the NinePatch.
+ * @param location Where to draw the NinePatch.
+ */
+ public void draw(Canvas canvas, Rect location) {
+ canvas.drawPatch(this, location, mPaint);
+ }
+
+ /**
+ * Draws the NinePatch. This method will ignore the paint returned
+ * by {@link #getPaint()} and use the specified paint instead.
+ *
+ * @param canvas A container for the current matrix and clip used to draw the NinePatch.
+ * @param location Where to draw the NinePatch.
+ * @param paint The Paint to draw through.
+ */
+ public void draw(Canvas canvas, Rect location, Paint paint) {
+ canvas.drawPatch(this, location, paint);
+ }
+
+ /**
+ * Return the underlying bitmap's density, as per
+ * {@link Bitmap#getDensity() Bitmap.getDensity()}.
+ */
+ public int getDensity() {
+ return mBitmap.mDensity;
+ }
+
+ /**
+ * Returns the intrinsic width, in pixels, of this NinePatch. This is equivalent
+ * to querying the width of the underlying bitmap returned by {@link #getBitmap()}.
+ */
+ public int getWidth() {
+ return mBitmap.getWidth();
+ }
+
+ /**
+ * Returns the intrinsic height, in pixels, of this NinePatch. This is equivalent
+ * to querying the height of the underlying bitmap returned by {@link #getBitmap()}.
+ */
+ public int getHeight() {
+ return mBitmap.getHeight();
+ }
+
+ /**
+ * Indicates whether this NinePatch contains transparent or translucent pixels.
+ * This is equivalent to calling <code>getBitmap().hasAlpha()</code> on this
+ * NinePatch.
+ */
+ public final boolean hasAlpha() {
+ return mBitmap.hasAlpha();
+ }
+
+ /**
+ * Returns a {@link Region} representing the parts of the NinePatch that are
+ * completely transparent.
+ *
+ * @param bounds The location and size of the NinePatch.
+ *
+ * @return null if the NinePatch has no transparent region to
+ * report, else a {@link Region} holding the parts of the specified bounds
+ * that are transparent.
+ */
+ public final Region getTransparentRegion(Rect bounds) {
+ long r = nativeGetTransparentRegion(mBitmap, mNativeChunk, bounds);
+ return r != 0 ? new Region(r) : null;
+ }
+
+ /**
+ * Verifies that the specified byte array is a valid 9-patch data chunk.
+ *
+ * @param chunk A byte array representing a 9-patch data chunk.
+ *
+ * @return True if the specified byte array represents a 9-patch data chunk,
+ * false otherwise.
+ */
+ public native static boolean isNinePatchChunk(byte[] chunk);
+
+ /**
+ * Validates the 9-patch chunk and throws an exception if the chunk is invalid.
+ * If validation is successful, this method returns a native Res_png_9patch*
+ * object used by the renderers.
+ */
+ private static native long validateNinePatchChunk(byte[] chunk);
+ private static native void nativeFinalize(long chunk);
+ private static native long nativeGetTransparentRegion(Bitmap bitmap, long chunk, Rect location);
+}
diff --git a/android/graphics/NinePatch_Delegate.java b/android/graphics/NinePatch_Delegate.java
new file mode 100644
index 0000000..43e5b0f
--- /dev/null
+++ b/android/graphics/NinePatch_Delegate.java
@@ -0,0 +1,181 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.ninepatch.NinePatchChunk;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.graphics.drawable.NinePatchDrawable;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.ref.SoftReference;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Delegate implementing the native methods of android.graphics.NinePatch
+ *
+ * Through the layoutlib_create tool, the original native methods of NinePatch have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager}
+ * around to map int to instance of the delegate.
+ *
+ */
+public final class NinePatch_Delegate {
+
+ // ---- delegate manager ----
+ private static final DelegateManager<NinePatch_Delegate> sManager =
+ new DelegateManager<>(NinePatch_Delegate.class);
+
+ // ---- delegate helper data ----
+ /**
+ * Cache map for {@link NinePatchChunk}.
+ * When the chunks are created they are serialized into a byte[], and both are put
+ * in the cache, using a {@link SoftReference} for the chunk. The default Java classes
+ * for {@link NinePatch} and {@link NinePatchDrawable} only reference to the byte[] data, and
+ * provide this for drawing.
+ * Using the cache map allows us to not have to deserialize the byte[] back into a
+ * {@link NinePatchChunk} every time a rendering is done.
+ */
+ private final static Map<byte[], SoftReference<NinePatchChunk>> sChunkCache = new HashMap<>();
+
+ // ---- delegate data ----
+ private byte[] chunk;
+
+
+ // ---- Public Helper methods ----
+
+ /**
+ * Serializes the given chunk.
+ *
+ * @return the serialized data for the chunk.
+ */
+ public static byte[] serialize(NinePatchChunk chunk) {
+ // serialize the chunk to get a byte[]
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = null;
+ try {
+ oos = new ObjectOutputStream(baos);
+ oos.writeObject(chunk);
+ } catch (IOException e) {
+ Bridge.getLog().error(null, "Failed to serialize NinePatchChunk.", e, null /*data*/);
+ return null;
+ } finally {
+ if (oos != null) {
+ try {
+ oos.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ // get the array and add it to the cache
+ byte[] array = baos.toByteArray();
+ sChunkCache.put(array, new SoftReference<>(chunk));
+ return array;
+ }
+
+ /**
+ * Returns a {@link NinePatchChunk} object for the given serialized representation.
+ *
+ * If the chunk is present in the cache then the object from the cache is returned, otherwise
+ * the array is deserialized into a {@link NinePatchChunk} object.
+ *
+ * @param array the serialized representation of the chunk.
+ * @return the NinePatchChunk or null if deserialization failed.
+ */
+ public static NinePatchChunk getChunk(byte[] array) {
+ SoftReference<NinePatchChunk> chunkRef = sChunkCache.get(array);
+ NinePatchChunk chunk = chunkRef.get();
+ if (chunk == null) {
+ ByteArrayInputStream bais = new ByteArrayInputStream(array);
+ ObjectInputStream ois = null;
+ try {
+ ois = new ObjectInputStream(bais);
+ chunk = (NinePatchChunk) ois.readObject();
+
+ // put back the chunk in the cache
+ if (chunk != null) {
+ sChunkCache.put(array, new SoftReference<>(chunk));
+ }
+ } catch (IOException e) {
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN,
+ "Failed to deserialize NinePatchChunk content.", e, null /*data*/);
+ return null;
+ } catch (ClassNotFoundException e) {
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN,
+ "Failed to deserialize NinePatchChunk class.", e, null /*data*/);
+ return null;
+ } finally {
+ if (ois != null) {
+ try {
+ ois.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
+ return chunk;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static boolean isNinePatchChunk(byte[] chunk) {
+ NinePatchChunk chunkObject = getChunk(chunk);
+ return chunkObject != null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long validateNinePatchChunk(byte[] chunk) {
+ // the default JNI implementation only checks that the byte[] has the same
+ // size as the C struct it represent. Since we cannot do the same check (serialization
+ // will return different size depending on content), we do nothing.
+ NinePatch_Delegate newDelegate = new NinePatch_Delegate();
+ newDelegate.chunk = chunk;
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeFinalize(long chunk) {
+ sManager.removeJavaReferenceFor(chunk);
+ }
+
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeGetTransparentRegion(Bitmap bitmap, long chunk, Rect location) {
+ return 0;
+ }
+
+ static byte[] getChunk(long nativeNinePatch) {
+ NinePatch_Delegate delegate = sManager.getDelegate(nativeNinePatch);
+ if (delegate != null) {
+ return delegate.chunk;
+ }
+ return null;
+ }
+
+}
diff --git a/android/graphics/Outline.java b/android/graphics/Outline.java
new file mode 100644
index 0000000..1c85df0
--- /dev/null
+++ b/android/graphics/Outline.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2014 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 android.graphics;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.graphics.drawable.Drawable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Defines a simple shape, used for bounding graphical regions.
+ * <p>
+ * Can be computed for a View, or computed by a Drawable, to drive the shape of
+ * shadows cast by a View, or to clip the contents of the View.
+ *
+ * @see android.view.ViewOutlineProvider
+ * @see android.view.View#setOutlineProvider(android.view.ViewOutlineProvider)
+ * @see Drawable#getOutline(Outline)
+ */
+public final class Outline {
+ private static final float RADIUS_UNDEFINED = Float.NEGATIVE_INFINITY;
+
+ /** @hide */
+ public static final int MODE_EMPTY = 0;
+ /** @hide */
+ public static final int MODE_ROUND_RECT = 1;
+ /** @hide */
+ public static final int MODE_CONVEX_PATH = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false,
+ value = {
+ MODE_EMPTY,
+ MODE_ROUND_RECT,
+ MODE_CONVEX_PATH,
+ })
+ public @interface Mode {}
+
+ /** @hide */
+ @Mode
+ public int mMode = MODE_EMPTY;
+
+ /**
+ * Only guaranteed to be non-null when mode == MODE_CONVEX_PATH
+ *
+ * @hide
+ */
+ public Path mPath;
+
+ /** @hide */
+ public final Rect mRect = new Rect();
+ /** @hide */
+ public float mRadius = RADIUS_UNDEFINED;
+ /** @hide */
+ public float mAlpha;
+
+ /**
+ * Constructs an empty Outline. Call one of the setter methods to make
+ * the outline valid for use with a View.
+ */
+ public Outline() {}
+
+ /**
+ * Constructs an Outline with a copy of the data in src.
+ */
+ public Outline(@NonNull Outline src) {
+ set(src);
+ }
+
+ /**
+ * Sets the outline to be empty.
+ *
+ * @see #isEmpty()
+ */
+ public void setEmpty() {
+ if (mPath != null) {
+ // rewind here to avoid thrashing the allocations, but could alternately clear ref
+ mPath.rewind();
+ }
+ mMode = MODE_EMPTY;
+ mRect.setEmpty();
+ mRadius = RADIUS_UNDEFINED;
+ }
+
+ /**
+ * Returns whether the Outline is empty.
+ * <p>
+ * Outlines are empty when constructed, or if {@link #setEmpty()} is called,
+ * until a setter method is called
+ *
+ * @see #setEmpty()
+ */
+ public boolean isEmpty() {
+ return mMode == MODE_EMPTY;
+ }
+
+
+ /**
+ * Returns whether the outline can be used to clip a View.
+ * <p>
+ * Currently, only Outlines that can be represented as a rectangle, circle,
+ * or round rect support clipping.
+ *
+ * @see android.view.View#setClipToOutline(boolean)
+ */
+ public boolean canClip() {
+ return mMode != MODE_CONVEX_PATH;
+ }
+
+ /**
+ * Sets the alpha represented by the Outline - the degree to which the
+ * producer is guaranteed to be opaque over the Outline's shape.
+ * <p>
+ * An alpha value of <code>0.0f</code> either represents completely
+ * transparent content, or content that isn't guaranteed to fill the shape
+ * it publishes.
+ * <p>
+ * Content producing a fully opaque (alpha = <code>1.0f</code>) outline is
+ * assumed by the drawing system to fully cover content beneath it,
+ * meaning content beneath may be optimized away.
+ */
+ public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {
+ mAlpha = alpha;
+ }
+
+ /**
+ * Returns the alpha represented by the Outline.
+ */
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ /**
+ * Replace the contents of this Outline with the contents of src.
+ *
+ * @param src Source outline to copy from.
+ */
+ public void set(@NonNull Outline src) {
+ mMode = src.mMode;
+ if (src.mMode == MODE_CONVEX_PATH) {
+ if (mPath == null) {
+ mPath = new Path();
+ }
+ mPath.set(src.mPath);
+ }
+ mRect.set(src.mRect);
+ mRadius = src.mRadius;
+ mAlpha = src.mAlpha;
+ }
+
+ /**
+ * Sets the Outline to the rounded rect defined by the input rect, and
+ * corner radius.
+ */
+ public void setRect(int left, int top, int right, int bottom) {
+ setRoundRect(left, top, right, bottom, 0.0f);
+ }
+
+ /**
+ * Convenience for {@link #setRect(int, int, int, int)}
+ */
+ public void setRect(@NonNull Rect rect) {
+ setRect(rect.left, rect.top, rect.right, rect.bottom);
+ }
+
+ /**
+ * Sets the Outline to the rounded rect defined by the input rect, and corner radius.
+ * <p>
+ * Passing a zero radius is equivalent to calling {@link #setRect(int, int, int, int)}
+ */
+ public void setRoundRect(int left, int top, int right, int bottom, float radius) {
+ if (left >= right || top >= bottom) {
+ setEmpty();
+ return;
+ }
+
+ if (mMode == MODE_CONVEX_PATH) {
+ // rewind here to avoid thrashing the allocations, but could alternately clear ref
+ mPath.rewind();
+ }
+ mMode = MODE_ROUND_RECT;
+ mRect.set(left, top, right, bottom);
+ mRadius = radius;
+ }
+
+ /**
+ * Convenience for {@link #setRoundRect(int, int, int, int, float)}
+ */
+ public void setRoundRect(@NonNull Rect rect, float radius) {
+ setRoundRect(rect.left, rect.top, rect.right, rect.bottom, radius);
+ }
+
+ /**
+ * Populates {@code outBounds} with the outline bounds, if set, and returns
+ * {@code true}. If no outline bounds are set, or if a path has been set
+ * via {@link #setConvexPath(Path)}, returns {@code false}.
+ *
+ * @param outRect the rect to populate with the outline bounds, if set
+ * @return {@code true} if {@code outBounds} was populated with outline
+ * bounds, or {@code false} if no outline bounds are set
+ */
+ public boolean getRect(@NonNull Rect outRect) {
+ if (mMode != MODE_ROUND_RECT) {
+ return false;
+ }
+ outRect.set(mRect);
+ return true;
+ }
+
+ /**
+ * Returns the rounded rect radius, if set, or a value less than 0 if a path has
+ * been set via {@link #setConvexPath(Path)}. A return value of {@code 0}
+ * indicates a non-rounded rect.
+ *
+ * @return the rounded rect radius, or value < 0
+ */
+ public float getRadius() {
+ return mRadius;
+ }
+
+ /**
+ * Sets the outline to the oval defined by input rect.
+ */
+ public void setOval(int left, int top, int right, int bottom) {
+ if (left >= right || top >= bottom) {
+ setEmpty();
+ return;
+ }
+
+ if ((bottom - top) == (right - left)) {
+ // represent circle as round rect, for efficiency, and to enable clipping
+ setRoundRect(left, top, right, bottom, (bottom - top) / 2.0f);
+ return;
+ }
+
+ if (mPath == null) {
+ mPath = new Path();
+ } else {
+ mPath.rewind();
+ }
+
+ mMode = MODE_CONVEX_PATH;
+ mPath.addOval(left, top, right, bottom, Path.Direction.CW);
+ mRect.setEmpty();
+ mRadius = RADIUS_UNDEFINED;
+ }
+
+ /**
+ * Convenience for {@link #setOval(int, int, int, int)}
+ */
+ public void setOval(@NonNull Rect rect) {
+ setOval(rect.left, rect.top, rect.right, rect.bottom);
+ }
+
+ /**
+ * Sets the Constructs an Outline from a
+ * {@link android.graphics.Path#isConvex() convex path}.
+ */
+ public void setConvexPath(@NonNull Path convexPath) {
+ if (convexPath.isEmpty()) {
+ setEmpty();
+ return;
+ }
+
+ if (!convexPath.isConvex()) {
+ throw new IllegalArgumentException("path must be convex");
+ }
+
+ if (mPath == null) {
+ mPath = new Path();
+ }
+
+ mMode = MODE_CONVEX_PATH;
+ mPath.set(convexPath);
+ mRect.setEmpty();
+ mRadius = RADIUS_UNDEFINED;
+ }
+
+ /**
+ * Offsets the Outline by (dx,dy)
+ */
+ public void offset(int dx, int dy) {
+ if (mMode == MODE_ROUND_RECT) {
+ mRect.offset(dx, dy);
+ } else if (mMode == MODE_CONVEX_PATH) {
+ mPath.offset(dx, dy);
+ }
+ }
+}
diff --git a/android/graphics/Paint.java b/android/graphics/Paint.java
new file mode 100644
index 0000000..1a06a56
--- /dev/null
+++ b/android/graphics/Paint.java
@@ -0,0 +1,3008 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Size;
+import android.graphics.FontListParser;
+import android.graphics.fonts.FontVariationAxis;
+import android.os.LocaleList;
+import android.text.FontConfig;
+import android.text.GraphicsOperations;
+import android.text.SpannableString;
+import android.text.SpannedString;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.GuardedBy;
+
+import dalvik.annotation.optimization.CriticalNative;
+import dalvik.annotation.optimization.FastNative;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+
+import libcore.util.NativeAllocationRegistry;
+
+/**
+ * The Paint class holds the style and color information about how to draw
+ * geometries, text and bitmaps.
+ */
+public class Paint {
+
+ private long mNativePaint;
+ private long mNativeShader;
+ private long mNativeColorFilter;
+
+ // The approximate size of a native paint object.
+ private static final long NATIVE_PAINT_SIZE = 98;
+
+ // Use a Holder to allow static initialization of Paint in the boot image.
+ private static class NoImagePreloadHolder {
+ public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
+ Paint.class.getClassLoader(), nGetNativeFinalizer(), NATIVE_PAINT_SIZE);
+ }
+
+ private ColorFilter mColorFilter;
+ private MaskFilter mMaskFilter;
+ private PathEffect mPathEffect;
+ private Shader mShader;
+ private Typeface mTypeface;
+ private Xfermode mXfermode;
+
+ private boolean mHasCompatScaling;
+ private float mCompatScaling;
+ private float mInvCompatScaling;
+
+ private LocaleList mLocales;
+ private String mFontFeatureSettings;
+ private String mFontVariationSettings;
+
+ private float mShadowLayerRadius;
+ private float mShadowLayerDx;
+ private float mShadowLayerDy;
+ private int mShadowLayerColor;
+
+ private static final Object sCacheLock = new Object();
+
+ /**
+ * Cache for the Minikin language list ID.
+ *
+ * A map from a string representation of the LocaleList to Minikin's language list ID.
+ */
+ @GuardedBy("sCacheLock")
+ private static final HashMap<String, Integer> sMinikinLangListIdCache = new HashMap<>();
+
+ /**
+ * @hide
+ */
+ public int mBidiFlags = BIDI_DEFAULT_LTR;
+
+ static final Style[] sStyleArray = {
+ Style.FILL, Style.STROKE, Style.FILL_AND_STROKE
+ };
+ static final Cap[] sCapArray = {
+ Cap.BUTT, Cap.ROUND, Cap.SQUARE
+ };
+ static final Join[] sJoinArray = {
+ Join.MITER, Join.ROUND, Join.BEVEL
+ };
+ static final Align[] sAlignArray = {
+ Align.LEFT, Align.CENTER, Align.RIGHT
+ };
+
+ /**
+ * Paint flag that enables antialiasing when drawing.
+ *
+ * <p>Enabling this flag will cause all draw operations that support
+ * antialiasing to use it.</p>
+ *
+ * @see #Paint(int)
+ * @see #setFlags(int)
+ */
+ public static final int ANTI_ALIAS_FLAG = 0x01;
+ /**
+ * Paint flag that enables bilinear sampling on scaled bitmaps.
+ *
+ * <p>If cleared, scaled bitmaps will be drawn with nearest neighbor
+ * sampling, likely resulting in artifacts. This should generally be on
+ * when drawing bitmaps, unless performance-bound (rendering to software
+ * canvas) or preferring pixelation artifacts to blurriness when scaling
+ * significantly.</p>
+ *
+ * <p>If bitmaps are scaled for device density at creation time (as
+ * resource bitmaps often are) the filtering will already have been
+ * done.</p>
+ *
+ * @see #Paint(int)
+ * @see #setFlags(int)
+ */
+ public static final int FILTER_BITMAP_FLAG = 0x02;
+ /**
+ * Paint flag that enables dithering when blitting.
+ *
+ * <p>Enabling this flag applies a dither to any blit operation where the
+ * target's colour space is more constrained than the source.
+ *
+ * @see #Paint(int)
+ * @see #setFlags(int)
+ */
+ public static final int DITHER_FLAG = 0x04;
+ /**
+ * Paint flag that applies an underline decoration to drawn text.
+ *
+ * @see #Paint(int)
+ * @see #setFlags(int)
+ */
+ public static final int UNDERLINE_TEXT_FLAG = 0x08;
+ /**
+ * Paint flag that applies a strike-through decoration to drawn text.
+ *
+ * @see #Paint(int)
+ * @see #setFlags(int)
+ */
+ public static final int STRIKE_THRU_TEXT_FLAG = 0x10;
+ /**
+ * Paint flag that applies a synthetic bolding effect to drawn text.
+ *
+ * <p>Enabling this flag will cause text draw operations to apply a
+ * simulated bold effect when drawing a {@link Typeface} that is not
+ * already bold.</p>
+ *
+ * @see #Paint(int)
+ * @see #setFlags(int)
+ */
+ public static final int FAKE_BOLD_TEXT_FLAG = 0x20;
+ /**
+ * Paint flag that enables smooth linear scaling of text.
+ *
+ * <p>Enabling this flag does not actually scale text, but rather adjusts
+ * text draw operations to deal gracefully with smooth adjustment of scale.
+ * When this flag is enabled, font hinting is disabled to prevent shape
+ * deformation between scale factors, and glyph caching is disabled due to
+ * the large number of glyph images that will be generated.</p>
+ *
+ * <p>{@link #SUBPIXEL_TEXT_FLAG} should be used in conjunction with this
+ * flag to prevent glyph positions from snapping to whole pixel values as
+ * scale factor is adjusted.</p>
+ *
+ * @see #Paint(int)
+ * @see #setFlags(int)
+ */
+ public static final int LINEAR_TEXT_FLAG = 0x40;
+ /**
+ * Paint flag that enables subpixel positioning of text.
+ *
+ * <p>Enabling this flag causes glyph advances to be computed with subpixel
+ * accuracy.</p>
+ *
+ * <p>This can be used with {@link #LINEAR_TEXT_FLAG} to prevent text from
+ * jittering during smooth scale transitions.</p>
+ *
+ * @see #Paint(int)
+ * @see #setFlags(int)
+ */
+ public static final int SUBPIXEL_TEXT_FLAG = 0x80;
+ /** Legacy Paint flag, no longer used. */
+ public static final int DEV_KERN_TEXT_FLAG = 0x100;
+ /** @hide bit mask for the flag enabling subpixel glyph rendering for text */
+ public static final int LCD_RENDER_TEXT_FLAG = 0x200;
+ /**
+ * Paint flag that enables the use of bitmap fonts when drawing text.
+ *
+ * <p>Disabling this flag will prevent text draw operations from using
+ * embedded bitmap strikes in fonts, causing fonts with both scalable
+ * outlines and bitmap strikes to draw only the scalable outlines, and
+ * fonts with only bitmap strikes to not draw at all.</p>
+ *
+ * @see #Paint(int)
+ * @see #setFlags(int)
+ */
+ public static final int EMBEDDED_BITMAP_TEXT_FLAG = 0x400;
+ /** @hide bit mask for the flag forcing freetype's autohinter on for text */
+ public static final int AUTO_HINTING_TEXT_FLAG = 0x800;
+ /** @hide bit mask for the flag enabling vertical rendering for text */
+ public static final int VERTICAL_TEXT_FLAG = 0x1000;
+
+ // These flags are always set on a new/reset paint, even if flags 0 is passed.
+ static final int HIDDEN_DEFAULT_PAINT_FLAGS = DEV_KERN_TEXT_FLAG | EMBEDDED_BITMAP_TEXT_FLAG;
+
+ /**
+ * Font hinter option that disables font hinting.
+ *
+ * @see #setHinting(int)
+ */
+ public static final int HINTING_OFF = 0x0;
+
+ /**
+ * Font hinter option that enables font hinting.
+ *
+ * @see #setHinting(int)
+ */
+ public static final int HINTING_ON = 0x1;
+
+ /**
+ * Bidi flag to set LTR paragraph direction.
+ *
+ * @hide
+ */
+ public static final int BIDI_LTR = 0x0;
+
+ /**
+ * Bidi flag to set RTL paragraph direction.
+ *
+ * @hide
+ */
+ public static final int BIDI_RTL = 0x1;
+
+ /**
+ * Bidi flag to detect paragraph direction via heuristics, defaulting to
+ * LTR.
+ *
+ * @hide
+ */
+ public static final int BIDI_DEFAULT_LTR = 0x2;
+
+ /**
+ * Bidi flag to detect paragraph direction via heuristics, defaulting to
+ * RTL.
+ *
+ * @hide
+ */
+ public static final int BIDI_DEFAULT_RTL = 0x3;
+
+ /**
+ * Bidi flag to override direction to all LTR (ignore bidi).
+ *
+ * @hide
+ */
+ public static final int BIDI_FORCE_LTR = 0x4;
+
+ /**
+ * Bidi flag to override direction to all RTL (ignore bidi).
+ *
+ * @hide
+ */
+ public static final int BIDI_FORCE_RTL = 0x5;
+
+ /**
+ * Maximum Bidi flag value.
+ * @hide
+ */
+ private static final int BIDI_MAX_FLAG_VALUE = BIDI_FORCE_RTL;
+
+ /**
+ * Mask for bidi flags.
+ * @hide
+ */
+ private static final int BIDI_FLAG_MASK = 0x7;
+
+ /**
+ * Flag for getTextRunAdvances indicating left-to-right run direction.
+ * @hide
+ */
+ public static final int DIRECTION_LTR = 0;
+
+ /**
+ * Flag for getTextRunAdvances indicating right-to-left run direction.
+ * @hide
+ */
+ public static final int DIRECTION_RTL = 1;
+
+ /**
+ * Option for getTextRunCursor to compute the valid cursor after
+ * offset or the limit of the context, whichever is less.
+ * @hide
+ */
+ public static final int CURSOR_AFTER = 0;
+
+ /**
+ * Option for getTextRunCursor to compute the valid cursor at or after
+ * the offset or the limit of the context, whichever is less.
+ * @hide
+ */
+ public static final int CURSOR_AT_OR_AFTER = 1;
+
+ /**
+ * Option for getTextRunCursor to compute the valid cursor before
+ * offset or the start of the context, whichever is greater.
+ * @hide
+ */
+ public static final int CURSOR_BEFORE = 2;
+
+ /**
+ * Option for getTextRunCursor to compute the valid cursor at or before
+ * offset or the start of the context, whichever is greater.
+ * @hide
+ */
+ public static final int CURSOR_AT_OR_BEFORE = 3;
+
+ /**
+ * Option for getTextRunCursor to return offset if the cursor at offset
+ * is valid, or -1 if it isn't.
+ * @hide
+ */
+ public static final int CURSOR_AT = 4;
+
+ /**
+ * Maximum cursor option value.
+ */
+ private static final int CURSOR_OPT_MAX_VALUE = CURSOR_AT;
+
+ /**
+ * Mask for hyphen edits that happen at the end of a line. Keep in sync with the definition in
+ * Minikin's Hyphenator.h.
+ * @hide
+ */
+ public static final int HYPHENEDIT_MASK_END_OF_LINE = 0x07;
+
+ /**
+ * Mask for hyphen edits that happen at the start of a line. Keep in sync with the definition in
+ * Minikin's Hyphenator.h.
+ * @hide
+ */
+ public static final int HYPHENEDIT_MASK_START_OF_LINE = 0x03 << 3;
+
+ /**
+ * The Style specifies if the primitive being drawn is filled, stroked, or
+ * both (in the same color). The default is FILL.
+ */
+ public enum Style {
+ /**
+ * Geometry and text drawn with this style will be filled, ignoring all
+ * stroke-related settings in the paint.
+ */
+ FILL (0),
+ /**
+ * Geometry and text drawn with this style will be stroked, respecting
+ * the stroke-related fields on the paint.
+ */
+ STROKE (1),
+ /**
+ * Geometry and text drawn with this style will be both filled and
+ * stroked at the same time, respecting the stroke-related fields on
+ * the paint. This mode can give unexpected results if the geometry
+ * is oriented counter-clockwise. This restriction does not apply to
+ * either FILL or STROKE.
+ */
+ FILL_AND_STROKE (2);
+
+ Style(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ /**
+ * The Cap specifies the treatment for the beginning and ending of
+ * stroked lines and paths. The default is BUTT.
+ */
+ public enum Cap {
+ /**
+ * The stroke ends with the path, and does not project beyond it.
+ */
+ BUTT (0),
+ /**
+ * The stroke projects out as a semicircle, with the center at the
+ * end of the path.
+ */
+ ROUND (1),
+ /**
+ * The stroke projects out as a square, with the center at the end
+ * of the path.
+ */
+ SQUARE (2);
+
+ private Cap(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ /**
+ * The Join specifies the treatment where lines and curve segments
+ * join on a stroked path. The default is MITER.
+ */
+ public enum Join {
+ /**
+ * The outer edges of a join meet at a sharp angle
+ */
+ MITER (0),
+ /**
+ * The outer edges of a join meet in a circular arc.
+ */
+ ROUND (1),
+ /**
+ * The outer edges of a join meet with a straight line
+ */
+ BEVEL (2);
+
+ private Join(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ /**
+ * Align specifies how drawText aligns its text relative to the
+ * [x,y] coordinates. The default is LEFT.
+ */
+ public enum Align {
+ /**
+ * The text is drawn to the right of the x,y origin
+ */
+ LEFT (0),
+ /**
+ * The text is drawn centered horizontally on the x,y origin
+ */
+ CENTER (1),
+ /**
+ * The text is drawn to the left of the x,y origin
+ */
+ RIGHT (2);
+
+ private Align(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ /**
+ * Create a new paint with default settings.
+ */
+ public Paint() {
+ this(0);
+ }
+
+ /**
+ * Create a new paint with the specified flags. Use setFlags() to change
+ * these after the paint is created.
+ *
+ * @param flags initial flag bits, as if they were passed via setFlags().
+ */
+ public Paint(int flags) {
+ mNativePaint = nInit();
+ NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
+ setFlags(flags | HIDDEN_DEFAULT_PAINT_FLAGS);
+ // TODO: Turning off hinting has undesirable side effects, we need to
+ // revisit hinting once we add support for subpixel positioning
+ // setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
+ // ? HINTING_OFF : HINTING_ON);
+ mCompatScaling = mInvCompatScaling = 1;
+ setTextLocales(LocaleList.getAdjustedDefault());
+ }
+
+ /**
+ * Create a new paint, initialized with the attributes in the specified
+ * paint parameter.
+ *
+ * @param paint Existing paint used to initialized the attributes of the
+ * new paint.
+ */
+ public Paint(Paint paint) {
+ mNativePaint = nInitWithPaint(paint.getNativeInstance());
+ NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
+ setClassVariablesFrom(paint);
+ }
+
+ /** Restores the paint to its default settings. */
+ public void reset() {
+ nReset(mNativePaint);
+ setFlags(HIDDEN_DEFAULT_PAINT_FLAGS);
+
+ // TODO: Turning off hinting has undesirable side effects, we need to
+ // revisit hinting once we add support for subpixel positioning
+ // setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
+ // ? HINTING_OFF : HINTING_ON);
+
+ mColorFilter = null;
+ mMaskFilter = null;
+ mPathEffect = null;
+ mShader = null;
+ mNativeShader = 0;
+ mTypeface = null;
+ mXfermode = null;
+
+ mHasCompatScaling = false;
+ mCompatScaling = 1;
+ mInvCompatScaling = 1;
+
+ mBidiFlags = BIDI_DEFAULT_LTR;
+ setTextLocales(LocaleList.getAdjustedDefault());
+ setElegantTextHeight(false);
+ mFontFeatureSettings = null;
+ mFontVariationSettings = null;
+
+ mShadowLayerRadius = 0.0f;
+ mShadowLayerDx = 0.0f;
+ mShadowLayerDy = 0.0f;
+ mShadowLayerColor = 0;
+ }
+
+ /**
+ * Copy the fields from src into this paint. This is equivalent to calling
+ * get() on all of the src fields, and calling the corresponding set()
+ * methods on this.
+ */
+ public void set(Paint src) {
+ if (this != src) {
+ // copy over the native settings
+ nSet(mNativePaint, src.mNativePaint);
+ setClassVariablesFrom(src);
+ }
+ }
+
+ /**
+ * Set all class variables using current values from the given
+ * {@link Paint}.
+ */
+ private void setClassVariablesFrom(Paint paint) {
+ mColorFilter = paint.mColorFilter;
+ mMaskFilter = paint.mMaskFilter;
+ mPathEffect = paint.mPathEffect;
+ mShader = paint.mShader;
+ mNativeShader = paint.mNativeShader;
+ mTypeface = paint.mTypeface;
+ mXfermode = paint.mXfermode;
+
+ mHasCompatScaling = paint.mHasCompatScaling;
+ mCompatScaling = paint.mCompatScaling;
+ mInvCompatScaling = paint.mInvCompatScaling;
+
+ mBidiFlags = paint.mBidiFlags;
+ mLocales = paint.mLocales;
+ mFontFeatureSettings = paint.mFontFeatureSettings;
+ mFontVariationSettings = paint.mFontVariationSettings;
+
+ mShadowLayerRadius = paint.mShadowLayerRadius;
+ mShadowLayerDx = paint.mShadowLayerDx;
+ mShadowLayerDy = paint.mShadowLayerDy;
+ mShadowLayerColor = paint.mShadowLayerColor;
+ }
+
+ /**
+ * Returns true if all attributes are equal.
+ *
+ * The caller is expected to have checked the trivial cases, like the pointers being equal,
+ * the objects having different classes, or the parameter being null.
+ * @hide
+ */
+ public boolean hasEqualAttributes(@NonNull Paint other) {
+ return mColorFilter == other.mColorFilter
+ && mMaskFilter == other.mMaskFilter
+ && mPathEffect == other.mPathEffect
+ && mShader == other.mShader
+ && mTypeface == other.mTypeface
+ && mXfermode == other.mXfermode
+ && mHasCompatScaling == other.mHasCompatScaling
+ && mCompatScaling == other.mCompatScaling
+ && mInvCompatScaling == other.mInvCompatScaling
+ && mBidiFlags == other.mBidiFlags
+ && mLocales.equals(other.mLocales)
+ && TextUtils.equals(mFontFeatureSettings, other.mFontFeatureSettings)
+ && TextUtils.equals(mFontVariationSettings, other.mFontVariationSettings)
+ && mShadowLayerRadius == other.mShadowLayerRadius
+ && mShadowLayerDx == other.mShadowLayerDx
+ && mShadowLayerDy == other.mShadowLayerDy
+ && mShadowLayerColor == other.mShadowLayerColor
+ && getFlags() == other.getFlags()
+ && getHinting() == other.getHinting()
+ && getStyle() == other.getStyle()
+ && getColor() == other.getColor()
+ && getStrokeWidth() == other.getStrokeWidth()
+ && getStrokeMiter() == other.getStrokeMiter()
+ && getStrokeCap() == other.getStrokeCap()
+ && getStrokeJoin() == other.getStrokeJoin()
+ && getTextAlign() == other.getTextAlign()
+ && isElegantTextHeight() == other.isElegantTextHeight()
+ && getTextSize() == other.getTextSize()
+ && getTextScaleX() == other.getTextScaleX()
+ && getTextSkewX() == other.getTextSkewX()
+ && getLetterSpacing() == other.getLetterSpacing()
+ && getWordSpacing() == other.getWordSpacing()
+ && getHyphenEdit() == other.getHyphenEdit();
+ }
+
+ /** @hide */
+ public void setCompatibilityScaling(float factor) {
+ if (factor == 1.0) {
+ mHasCompatScaling = false;
+ mCompatScaling = mInvCompatScaling = 1.0f;
+ } else {
+ mHasCompatScaling = true;
+ mCompatScaling = factor;
+ mInvCompatScaling = 1.0f/factor;
+ }
+ }
+
+ /**
+ * Return the pointer to the native object while ensuring that any
+ * mutable objects that are attached to the paint are also up-to-date.
+ *
+ * @hide
+ */
+ public long getNativeInstance() {
+ long newNativeShader = mShader == null ? 0 : mShader.getNativeInstance();
+ if (newNativeShader != mNativeShader) {
+ mNativeShader = newNativeShader;
+ nSetShader(mNativePaint, mNativeShader);
+ }
+ long newNativeColorFilter = mColorFilter == null ? 0 : mColorFilter.getNativeInstance();
+ if (newNativeColorFilter != mNativeColorFilter) {
+ mNativeColorFilter = newNativeColorFilter;
+ nSetColorFilter(mNativePaint, mNativeColorFilter);
+ }
+ return mNativePaint;
+ }
+
+ /**
+ * Return the bidi flags on the paint.
+ *
+ * @return the bidi flags on the paint
+ * @hide
+ */
+ public int getBidiFlags() {
+ return mBidiFlags;
+ }
+
+ /**
+ * Set the bidi flags on the paint.
+ * @hide
+ */
+ public void setBidiFlags(int flags) {
+ // only flag value is the 3-bit BIDI control setting
+ flags &= BIDI_FLAG_MASK;
+ if (flags > BIDI_MAX_FLAG_VALUE) {
+ throw new IllegalArgumentException("unknown bidi flag: " + flags);
+ }
+ mBidiFlags = flags;
+ }
+
+ /**
+ * Return the paint's flags. Use the Flag enum to test flag values.
+ *
+ * @return the paint's flags (see enums ending in _Flag for bit masks)
+ */
+ public int getFlags() {
+ return nGetFlags(mNativePaint);
+ }
+
+ /**
+ * Set the paint's flags. Use the Flag enum to specific flag values.
+ *
+ * @param flags The new flag bits for the paint
+ */
+ public void setFlags(int flags) {
+ nSetFlags(mNativePaint, flags);
+ }
+
+ /**
+ * Return the paint's hinting mode. Returns either
+ * {@link #HINTING_OFF} or {@link #HINTING_ON}.
+ */
+ public int getHinting() {
+ return nGetHinting(mNativePaint);
+ }
+
+ /**
+ * Set the paint's hinting mode. May be either
+ * {@link #HINTING_OFF} or {@link #HINTING_ON}.
+ */
+ public void setHinting(int mode) {
+ nSetHinting(mNativePaint, mode);
+ }
+
+ /**
+ * Helper for getFlags(), returning true if ANTI_ALIAS_FLAG bit is set
+ * AntiAliasing smooths out the edges of what is being drawn, but is has
+ * no impact on the interior of the shape. See setDither() and
+ * setFilterBitmap() to affect how colors are treated.
+ *
+ * @return true if the antialias bit is set in the paint's flags.
+ */
+ public final boolean isAntiAlias() {
+ return (getFlags() & ANTI_ALIAS_FLAG) != 0;
+ }
+
+ /**
+ * Helper for setFlags(), setting or clearing the ANTI_ALIAS_FLAG bit
+ * AntiAliasing smooths out the edges of what is being drawn, but is has
+ * no impact on the interior of the shape. See setDither() and
+ * setFilterBitmap() to affect how colors are treated.
+ *
+ * @param aa true to set the antialias bit in the flags, false to clear it
+ */
+ public void setAntiAlias(boolean aa) {
+ nSetAntiAlias(mNativePaint, aa);
+ }
+
+ /**
+ * Helper for getFlags(), returning true if DITHER_FLAG bit is set
+ * Dithering affects how colors that are higher precision than the device
+ * are down-sampled. No dithering is generally faster, but higher precision
+ * colors are just truncated down (e.g. 8888 -> 565). Dithering tries to
+ * distribute the error inherent in this process, to reduce the visual
+ * artifacts.
+ *
+ * @return true if the dithering bit is set in the paint's flags.
+ */
+ public final boolean isDither() {
+ return (getFlags() & DITHER_FLAG) != 0;
+ }
+
+ /**
+ * Helper for setFlags(), setting or clearing the DITHER_FLAG bit
+ * Dithering affects how colors that are higher precision than the device
+ * are down-sampled. No dithering is generally faster, but higher precision
+ * colors are just truncated down (e.g. 8888 -> 565). Dithering tries to
+ * distribute the error inherent in this process, to reduce the visual
+ * artifacts.
+ *
+ * @param dither true to set the dithering bit in flags, false to clear it
+ */
+ public void setDither(boolean dither) {
+ nSetDither(mNativePaint, dither);
+ }
+
+ /**
+ * Helper for getFlags(), returning true if LINEAR_TEXT_FLAG bit is set
+ *
+ * @return true if the lineartext bit is set in the paint's flags
+ */
+ public final boolean isLinearText() {
+ return (getFlags() & LINEAR_TEXT_FLAG) != 0;
+ }
+
+ /**
+ * Helper for setFlags(), setting or clearing the LINEAR_TEXT_FLAG bit
+ *
+ * @param linearText true to set the linearText bit in the paint's flags,
+ * false to clear it.
+ */
+ public void setLinearText(boolean linearText) {
+ nSetLinearText(mNativePaint, linearText);
+ }
+
+ /**
+ * Helper for getFlags(), returning true if SUBPIXEL_TEXT_FLAG bit is set
+ *
+ * @return true if the subpixel bit is set in the paint's flags
+ */
+ public final boolean isSubpixelText() {
+ return (getFlags() & SUBPIXEL_TEXT_FLAG) != 0;
+ }
+
+ /**
+ * Helper for setFlags(), setting or clearing the SUBPIXEL_TEXT_FLAG bit
+ *
+ * @param subpixelText true to set the subpixelText bit in the paint's
+ * flags, false to clear it.
+ */
+ public void setSubpixelText(boolean subpixelText) {
+ nSetSubpixelText(mNativePaint, subpixelText);
+ }
+
+ /**
+ * Helper for getFlags(), returning true if UNDERLINE_TEXT_FLAG bit is set
+ *
+ * @return true if the underlineText bit is set in the paint's flags.
+ */
+ public final boolean isUnderlineText() {
+ return (getFlags() & UNDERLINE_TEXT_FLAG) != 0;
+ }
+
+ /**
+ * Distance from top of the underline to the baseline. Positive values mean below the baseline.
+ * This method returns where the underline should be drawn independent of if the underlineText
+ * bit is set at the moment.
+ * @hide
+ */
+ public float getUnderlinePosition() {
+ return nGetUnderlinePosition(mNativePaint);
+ }
+
+ /**
+ * @hide
+ */
+ public float getUnderlineThickness() {
+ return nGetUnderlineThickness(mNativePaint);
+ }
+
+ /**
+ * Helper for setFlags(), setting or clearing the UNDERLINE_TEXT_FLAG bit
+ *
+ * @param underlineText true to set the underlineText bit in the paint's
+ * flags, false to clear it.
+ */
+ public void setUnderlineText(boolean underlineText) {
+ nSetUnderlineText(mNativePaint, underlineText);
+ }
+
+ /**
+ * Helper for getFlags(), returning true if STRIKE_THRU_TEXT_FLAG bit is set
+ *
+ * @return true if the strikeThruText bit is set in the paint's flags.
+ */
+ public final boolean isStrikeThruText() {
+ return (getFlags() & STRIKE_THRU_TEXT_FLAG) != 0;
+ }
+
+ /**
+ * Distance from top of the strike-through line to the baseline. Negative values mean above the
+ * baseline. This method returns where the strike-through line should be drawn independent of if
+ * the strikeThruText bit is set at the moment.
+ * @hide
+ */
+ public float getStrikeThruPosition() {
+ return nGetStrikeThruPosition(mNativePaint);
+ }
+
+ /**
+ * @hide
+ */
+ public float getStrikeThruThickness() {
+ return nGetStrikeThruThickness(mNativePaint);
+ }
+
+ /**
+ * Helper for setFlags(), setting or clearing the STRIKE_THRU_TEXT_FLAG bit
+ *
+ * @param strikeThruText true to set the strikeThruText bit in the paint's
+ * flags, false to clear it.
+ */
+ public void setStrikeThruText(boolean strikeThruText) {
+ nSetStrikeThruText(mNativePaint, strikeThruText);
+ }
+
+ /**
+ * Helper for getFlags(), returning true if FAKE_BOLD_TEXT_FLAG bit is set
+ *
+ * @return true if the fakeBoldText bit is set in the paint's flags.
+ */
+ public final boolean isFakeBoldText() {
+ return (getFlags() & FAKE_BOLD_TEXT_FLAG) != 0;
+ }
+
+ /**
+ * Helper for setFlags(), setting or clearing the FAKE_BOLD_TEXT_FLAG bit
+ *
+ * @param fakeBoldText true to set the fakeBoldText bit in the paint's
+ * flags, false to clear it.
+ */
+ public void setFakeBoldText(boolean fakeBoldText) {
+ nSetFakeBoldText(mNativePaint, fakeBoldText);
+ }
+
+ /**
+ * Whether or not the bitmap filter is activated.
+ * Filtering affects the sampling of bitmaps when they are transformed.
+ * Filtering does not affect how the colors in the bitmap are converted into
+ * device pixels. That is dependent on dithering and xfermodes.
+ *
+ * @see #setFilterBitmap(boolean) setFilterBitmap()
+ */
+ public final boolean isFilterBitmap() {
+ return (getFlags() & FILTER_BITMAP_FLAG) != 0;
+ }
+
+ /**
+ * Helper for setFlags(), setting or clearing the FILTER_BITMAP_FLAG bit.
+ * Filtering affects the sampling of bitmaps when they are transformed.
+ * Filtering does not affect how the colors in the bitmap are converted into
+ * device pixels. That is dependent on dithering and xfermodes.
+ *
+ * @param filter true to set the FILTER_BITMAP_FLAG bit in the paint's
+ * flags, false to clear it.
+ */
+ public void setFilterBitmap(boolean filter) {
+ nSetFilterBitmap(mNativePaint, filter);
+ }
+
+ /**
+ * Return the paint's style, used for controlling how primitives'
+ * geometries are interpreted (except for drawBitmap, which always assumes
+ * FILL_STYLE).
+ *
+ * @return the paint's style setting (Fill, Stroke, StrokeAndFill)
+ */
+ public Style getStyle() {
+ return sStyleArray[nGetStyle(mNativePaint)];
+ }
+
+ /**
+ * Set the paint's style, used for controlling how primitives'
+ * geometries are interpreted (except for drawBitmap, which always assumes
+ * Fill).
+ *
+ * @param style The new style to set in the paint
+ */
+ public void setStyle(Style style) {
+ nSetStyle(mNativePaint, style.nativeInt);
+ }
+
+ /**
+ * Return the paint's color. Note that the color is a 32bit value
+ * containing alpha as well as r,g,b. This 32bit value is not premultiplied,
+ * meaning that its alpha can be any value, regardless of the values of
+ * r,g,b. See the Color class for more details.
+ *
+ * @return the paint's color (and alpha).
+ */
+ @ColorInt
+ public int getColor() {
+ return nGetColor(mNativePaint);
+ }
+
+ /**
+ * Set the paint's color. Note that the color is an int containing alpha
+ * as well as r,g,b. This 32bit value is not premultiplied, meaning that
+ * its alpha can be any value, regardless of the values of r,g,b.
+ * See the Color class for more details.
+ *
+ * @param color The new color (including alpha) to set in the paint.
+ */
+ public void setColor(@ColorInt int color) {
+ nSetColor(mNativePaint, color);
+ }
+
+ /**
+ * Helper to getColor() that just returns the color's alpha value. This is
+ * the same as calling getColor() >>> 24. It always returns a value between
+ * 0 (completely transparent) and 255 (completely opaque).
+ *
+ * @return the alpha component of the paint's color.
+ */
+ public int getAlpha() {
+ return nGetAlpha(mNativePaint);
+ }
+
+ /**
+ * Helper to setColor(), that only assigns the color's alpha value,
+ * leaving its r,g,b values unchanged. Results are undefined if the alpha
+ * value is outside of the range [0..255]
+ *
+ * @param a set the alpha component [0..255] of the paint's color.
+ */
+ public void setAlpha(int a) {
+ nSetAlpha(mNativePaint, a);
+ }
+
+ /**
+ * Helper to setColor(), that takes a,r,g,b and constructs the color int
+ *
+ * @param a The new alpha component (0..255) of the paint's color.
+ * @param r The new red component (0..255) of the paint's color.
+ * @param g The new green component (0..255) of the paint's color.
+ * @param b The new blue component (0..255) of the paint's color.
+ */
+ public void setARGB(int a, int r, int g, int b) {
+ setColor((a << 24) | (r << 16) | (g << 8) | b);
+ }
+
+ /**
+ * Return the width for stroking.
+ * <p />
+ * A value of 0 strokes in hairline mode.
+ * Hairlines always draws a single pixel independent of the canva's matrix.
+ *
+ * @return the paint's stroke width, used whenever the paint's style is
+ * Stroke or StrokeAndFill.
+ */
+ public float getStrokeWidth() {
+ return nGetStrokeWidth(mNativePaint);
+ }
+
+ /**
+ * Set the width for stroking.
+ * Pass 0 to stroke in hairline mode.
+ * Hairlines always draws a single pixel independent of the canva's matrix.
+ *
+ * @param width set the paint's stroke width, used whenever the paint's
+ * style is Stroke or StrokeAndFill.
+ */
+ public void setStrokeWidth(float width) {
+ nSetStrokeWidth(mNativePaint, width);
+ }
+
+ /**
+ * Return the paint's stroke miter value. Used to control the behavior
+ * of miter joins when the joins angle is sharp.
+ *
+ * @return the paint's miter limit, used whenever the paint's style is
+ * Stroke or StrokeAndFill.
+ */
+ public float getStrokeMiter() {
+ return nGetStrokeMiter(mNativePaint);
+ }
+
+ /**
+ * Set the paint's stroke miter value. This is used to control the behavior
+ * of miter joins when the joins angle is sharp. This value must be >= 0.
+ *
+ * @param miter set the miter limit on the paint, used whenever the paint's
+ * style is Stroke or StrokeAndFill.
+ */
+ public void setStrokeMiter(float miter) {
+ nSetStrokeMiter(mNativePaint, miter);
+ }
+
+ /**
+ * Return the paint's Cap, controlling how the start and end of stroked
+ * lines and paths are treated.
+ *
+ * @return the line cap style for the paint, used whenever the paint's
+ * style is Stroke or StrokeAndFill.
+ */
+ public Cap getStrokeCap() {
+ return sCapArray[nGetStrokeCap(mNativePaint)];
+ }
+
+ /**
+ * Set the paint's Cap.
+ *
+ * @param cap set the paint's line cap style, used whenever the paint's
+ * style is Stroke or StrokeAndFill.
+ */
+ public void setStrokeCap(Cap cap) {
+ nSetStrokeCap(mNativePaint, cap.nativeInt);
+ }
+
+ /**
+ * Return the paint's stroke join type.
+ *
+ * @return the paint's Join.
+ */
+ public Join getStrokeJoin() {
+ return sJoinArray[nGetStrokeJoin(mNativePaint)];
+ }
+
+ /**
+ * Set the paint's Join.
+ *
+ * @param join set the paint's Join, used whenever the paint's style is
+ * Stroke or StrokeAndFill.
+ */
+ public void setStrokeJoin(Join join) {
+ nSetStrokeJoin(mNativePaint, join.nativeInt);
+ }
+
+ /**
+ * Applies any/all effects (patheffect, stroking) to src, returning the
+ * result in dst. The result is that drawing src with this paint will be
+ * the same as drawing dst with a default paint (at least from the
+ * geometric perspective).
+ *
+ * @param src input path
+ * @param dst output path (may be the same as src)
+ * @return true if the path should be filled, or false if it should be
+ * drawn with a hairline (width == 0)
+ */
+ public boolean getFillPath(Path src, Path dst) {
+ return nGetFillPath(mNativePaint, src.readOnlyNI(), dst.mutateNI());
+ }
+
+ /**
+ * Get the paint's shader object.
+ *
+ * @return the paint's shader (or null)
+ */
+ public Shader getShader() {
+ return mShader;
+ }
+
+ /**
+ * Set or clear the shader object.
+ * <p />
+ * Pass null to clear any previous shader.
+ * As a convenience, the parameter passed is also returned.
+ *
+ * @param shader May be null. the new shader to be installed in the paint
+ * @return shader
+ */
+ public Shader setShader(Shader shader) {
+ // If mShader changes, cached value of native shader aren't valid, since
+ // old shader's pointer may be reused by another shader allocation later
+ if (mShader != shader) {
+ mNativeShader = -1;
+ // Release any native references to the old shader content
+ nSetShader(mNativePaint, 0);
+ }
+ // Defer setting the shader natively until getNativeInstance() is called
+ mShader = shader;
+ return shader;
+ }
+
+ /**
+ * Get the paint's colorfilter (maybe be null).
+ *
+ * @return the paint's colorfilter (maybe be null)
+ */
+ public ColorFilter getColorFilter() {
+ return mColorFilter;
+ }
+
+ /**
+ * Set or clear the paint's colorfilter, returning the parameter.
+ *
+ * @param filter May be null. The new filter to be installed in the paint
+ * @return filter
+ */
+ public ColorFilter setColorFilter(ColorFilter filter) {
+ // If mColorFilter changes, cached value of native shader aren't valid, since
+ // old shader's pointer may be reused by another shader allocation later
+ if (mColorFilter != filter) {
+ mNativeColorFilter = -1;
+ }
+
+ // Defer setting the filter natively until getNativeInstance() is called
+ mColorFilter = filter;
+ return filter;
+ }
+
+ /**
+ * Get the paint's transfer mode object.
+ *
+ * @return the paint's transfer mode (or null)
+ */
+ public Xfermode getXfermode() {
+ return mXfermode;
+ }
+
+ /**
+ * Set or clear the transfer mode object. A transfer mode defines how
+ * source pixels (generate by a drawing command) are composited with
+ * the destination pixels (content of the render target).
+ * <p />
+ * Pass null to clear any previous transfer mode.
+ * As a convenience, the parameter passed is also returned.
+ * <p />
+ * {@link PorterDuffXfermode} is the most common transfer mode.
+ *
+ * @param xfermode May be null. The xfermode to be installed in the paint
+ * @return xfermode
+ */
+ public Xfermode setXfermode(Xfermode xfermode) {
+ int newMode = xfermode != null ? xfermode.porterDuffMode : Xfermode.DEFAULT;
+ int curMode = mXfermode != null ? mXfermode.porterDuffMode : Xfermode.DEFAULT;
+ if (newMode != curMode) {
+ nSetXfermode(mNativePaint, newMode);
+ }
+ mXfermode = xfermode;
+ return xfermode;
+ }
+
+ /**
+ * Get the paint's patheffect object.
+ *
+ * @return the paint's patheffect (or null)
+ */
+ public PathEffect getPathEffect() {
+ return mPathEffect;
+ }
+
+ /**
+ * Set or clear the patheffect object.
+ * <p />
+ * Pass null to clear any previous patheffect.
+ * As a convenience, the parameter passed is also returned.
+ *
+ * @param effect May be null. The patheffect to be installed in the paint
+ * @return effect
+ */
+ public PathEffect setPathEffect(PathEffect effect) {
+ long effectNative = 0;
+ if (effect != null) {
+ effectNative = effect.native_instance;
+ }
+ nSetPathEffect(mNativePaint, effectNative);
+ mPathEffect = effect;
+ return effect;
+ }
+
+ /**
+ * Get the paint's maskfilter object.
+ *
+ * @return the paint's maskfilter (or null)
+ */
+ public MaskFilter getMaskFilter() {
+ return mMaskFilter;
+ }
+
+ /**
+ * Set or clear the maskfilter object.
+ * <p />
+ * Pass null to clear any previous maskfilter.
+ * As a convenience, the parameter passed is also returned.
+ *
+ * @param maskfilter May be null. The maskfilter to be installed in the
+ * paint
+ * @return maskfilter
+ */
+ public MaskFilter setMaskFilter(MaskFilter maskfilter) {
+ long maskfilterNative = 0;
+ if (maskfilter != null) {
+ maskfilterNative = maskfilter.native_instance;
+ }
+ nSetMaskFilter(mNativePaint, maskfilterNative);
+ mMaskFilter = maskfilter;
+ return maskfilter;
+ }
+
+ /**
+ * Get the paint's typeface object.
+ * <p />
+ * The typeface object identifies which font to use when drawing or
+ * measuring text.
+ *
+ * @return the paint's typeface (or null)
+ */
+ public Typeface getTypeface() {
+ return mTypeface;
+ }
+
+ /**
+ * Set or clear the typeface object.
+ * <p />
+ * Pass null to clear any previous typeface.
+ * As a convenience, the parameter passed is also returned.
+ *
+ * @param typeface May be null. The typeface to be installed in the paint
+ * @return typeface
+ */
+ public Typeface setTypeface(Typeface typeface) {
+ final long typefaceNative = typeface == null ? 0 : typeface.native_instance;
+ nSetTypeface(mNativePaint, typefaceNative);
+ mTypeface = typeface;
+ return typeface;
+ }
+
+ /**
+ * Get the paint's rasterizer (or null).
+ * <p />
+ * The raster controls/modifies how paths/text are turned into alpha masks.
+ *
+ * @return the paint's rasterizer (or null)
+ *
+ * @deprecated Rasterizer is not supported by either the HW or PDF backends.
+ * @removed
+ */
+ @Deprecated
+ public Rasterizer getRasterizer() {
+ return null;
+ }
+
+ /**
+ * Set or clear the rasterizer object.
+ * <p />
+ * Pass null to clear any previous rasterizer.
+ * As a convenience, the parameter passed is also returned.
+ *
+ * @param rasterizer May be null. The new rasterizer to be installed in
+ * the paint.
+ * @return rasterizer
+ *
+ * @deprecated Rasterizer is not supported by either the HW or PDF backends.
+ * @removed
+ */
+ @Deprecated
+ public Rasterizer setRasterizer(Rasterizer rasterizer) {
+ return rasterizer;
+ }
+
+ /**
+ * This draws a shadow layer below the main layer, with the specified
+ * offset and color, and blur radius. If radius is 0, then the shadow
+ * layer is removed.
+ * <p>
+ * Can be used to create a blurred shadow underneath text. Support for use
+ * with other drawing operations is constrained to the software rendering
+ * pipeline.
+ * <p>
+ * The alpha of the shadow will be the paint's alpha if the shadow color is
+ * opaque, or the alpha from the shadow color if not.
+ */
+ public void setShadowLayer(float radius, float dx, float dy, int shadowColor) {
+ mShadowLayerRadius = radius;
+ mShadowLayerDx = dx;
+ mShadowLayerDy = dy;
+ mShadowLayerColor = shadowColor;
+ nSetShadowLayer(mNativePaint, radius, dx, dy, shadowColor);
+ }
+
+ /**
+ * Clear the shadow layer.
+ */
+ public void clearShadowLayer() {
+ setShadowLayer(0, 0, 0, 0);
+ }
+
+ /**
+ * Checks if the paint has a shadow layer attached
+ *
+ * @return true if the paint has a shadow layer attached and false otherwise
+ * @hide
+ */
+ public boolean hasShadowLayer() {
+ return nHasShadowLayer(mNativePaint);
+ }
+
+ /**
+ * Return the paint's Align value for drawing text. This controls how the
+ * text is positioned relative to its origin. LEFT align means that all of
+ * the text will be drawn to the right of its origin (i.e. the origin
+ * specifieds the LEFT edge of the text) and so on.
+ *
+ * @return the paint's Align value for drawing text.
+ */
+ public Align getTextAlign() {
+ return sAlignArray[nGetTextAlign(mNativePaint)];
+ }
+
+ /**
+ * Set the paint's text alignment. This controls how the
+ * text is positioned relative to its origin. LEFT align means that all of
+ * the text will be drawn to the right of its origin (i.e. the origin
+ * specifieds the LEFT edge of the text) and so on.
+ *
+ * @param align set the paint's Align value for drawing text.
+ */
+ public void setTextAlign(Align align) {
+ nSetTextAlign(mNativePaint, align.nativeInt);
+ }
+
+ /**
+ * Get the text's primary Locale. Note that this is not all of the locale-related information
+ * Paint has. Use {@link #getTextLocales()} to get the complete list.
+ *
+ * @return the paint's primary Locale used for drawing text, never null.
+ */
+ @NonNull
+ public Locale getTextLocale() {
+ return mLocales.get(0);
+ }
+
+ /**
+ * Get the text locale list.
+ *
+ * @return the paint's LocaleList used for drawing text, never null or empty.
+ */
+ @NonNull @Size(min=1)
+ public LocaleList getTextLocales() {
+ return mLocales;
+ }
+
+ /**
+ * Set the text locale list to a one-member list consisting of just the locale.
+ *
+ * See {@link #setTextLocales(LocaleList)} for how the locale list affects
+ * the way the text is drawn for some languages.
+ *
+ * @param locale the paint's locale value for drawing text, must not be null.
+ */
+ public void setTextLocale(@NonNull Locale locale) {
+ if (locale == null) {
+ throw new IllegalArgumentException("locale cannot be null");
+ }
+ if (mLocales != null && mLocales.size() == 1 && locale.equals(mLocales.get(0))) {
+ return;
+ }
+ mLocales = new LocaleList(locale);
+ syncTextLocalesWithMinikin();
+ }
+
+ /**
+ * Set the text locale list.
+ *
+ * The text locale list affects how the text is drawn for some languages.
+ *
+ * For example, if the locale list contains {@link Locale#CHINESE} or {@link Locale#CHINA},
+ * then the text renderer will prefer to draw text using a Chinese font. Likewise,
+ * if the locale list contains {@link Locale#JAPANESE} or {@link Locale#JAPAN}, then the text
+ * renderer will prefer to draw text using a Japanese font. If the locale list contains both,
+ * the order those locales appear in the list is considered for deciding the font.
+ *
+ * This distinction is important because Chinese and Japanese text both use many
+ * of the same Unicode code points but their appearance is subtly different for
+ * each language.
+ *
+ * By default, the text locale list is initialized to a one-member list just containing the
+ * system locales. This assumes that the text to be rendered will most likely be in the user's
+ * preferred language.
+ *
+ * If the actual language or languages of the text is/are known, then they can be provided to
+ * the text renderer using this method. The text renderer may attempt to guess the
+ * language script based on the contents of the text to be drawn independent of
+ * the text locale here. Specifying the text locales just helps it do a better
+ * job in certain ambiguous cases.
+ *
+ * @param locales the paint's locale list for drawing text, must not be null or empty.
+ */
+ public void setTextLocales(@NonNull @Size(min=1) LocaleList locales) {
+ if (locales == null || locales.isEmpty()) {
+ throw new IllegalArgumentException("locales cannot be null or empty");
+ }
+ if (locales.equals(mLocales)) return;
+ mLocales = locales;
+ syncTextLocalesWithMinikin();
+ }
+
+ private void syncTextLocalesWithMinikin() {
+ final String languageTags = mLocales.toLanguageTags();
+ final Integer minikinLangListId;
+ synchronized (sCacheLock) {
+ minikinLangListId = sMinikinLangListIdCache.get(languageTags);
+ if (minikinLangListId == null) {
+ final int newID = nSetTextLocales(mNativePaint, languageTags);
+ sMinikinLangListIdCache.put(languageTags, newID);
+ return;
+ }
+ }
+ nSetTextLocalesByMinikinLangListId(mNativePaint, minikinLangListId.intValue());
+ }
+
+ /**
+ * Get the elegant metrics flag.
+ *
+ * @return true if elegant metrics are enabled for text drawing.
+ */
+ public boolean isElegantTextHeight() {
+ return nIsElegantTextHeight(mNativePaint);
+ }
+
+ /**
+ * Set the paint's elegant height metrics flag. This setting selects font
+ * variants that have not been compacted to fit Latin-based vertical
+ * metrics, and also increases top and bottom bounds to provide more space.
+ *
+ * @param elegant set the paint's elegant metrics flag for drawing text.
+ */
+ public void setElegantTextHeight(boolean elegant) {
+ nSetElegantTextHeight(mNativePaint, elegant);
+ }
+
+ /**
+ * Return the paint's text size.
+ *
+ * @return the paint's text size in pixel units.
+ */
+ public float getTextSize() {
+ return nGetTextSize(mNativePaint);
+ }
+
+ /**
+ * Set the paint's text size. This value must be > 0
+ *
+ * @param textSize set the paint's text size in pixel units.
+ */
+ public void setTextSize(float textSize) {
+ nSetTextSize(mNativePaint, textSize);
+ }
+
+ /**
+ * Return the paint's horizontal scale factor for text. The default value
+ * is 1.0.
+ *
+ * @return the paint's scale factor in X for drawing/measuring text
+ */
+ public float getTextScaleX() {
+ return nGetTextScaleX(mNativePaint);
+ }
+
+ /**
+ * Set the paint's horizontal scale factor for text. The default value
+ * is 1.0. Values > 1.0 will stretch the text wider. Values < 1.0 will
+ * stretch the text narrower.
+ *
+ * @param scaleX set the paint's scale in X for drawing/measuring text.
+ */
+ public void setTextScaleX(float scaleX) {
+ nSetTextScaleX(mNativePaint, scaleX);
+ }
+
+ /**
+ * Return the paint's horizontal skew factor for text. The default value
+ * is 0.
+ *
+ * @return the paint's skew factor in X for drawing text.
+ */
+ public float getTextSkewX() {
+ return nGetTextSkewX(mNativePaint);
+ }
+
+ /**
+ * Set the paint's horizontal skew factor for text. The default value
+ * is 0. For approximating oblique text, use values around -0.25.
+ *
+ * @param skewX set the paint's skew factor in X for drawing text.
+ */
+ public void setTextSkewX(float skewX) {
+ nSetTextSkewX(mNativePaint, skewX);
+ }
+
+ /**
+ * Return the paint's letter-spacing for text. The default value
+ * is 0.
+ *
+ * @return the paint's letter-spacing for drawing text.
+ */
+ public float getLetterSpacing() {
+ return nGetLetterSpacing(mNativePaint);
+ }
+
+ /**
+ * Set the paint's letter-spacing for text. The default value
+ * is 0. The value is in 'EM' units. Typical values for slight
+ * expansion will be around 0.05. Negative values tighten text.
+ *
+ * @param letterSpacing set the paint's letter-spacing for drawing text.
+ */
+ public void setLetterSpacing(float letterSpacing) {
+ nSetLetterSpacing(mNativePaint, letterSpacing);
+ }
+
+ /**
+ * Return the paint's word-spacing for text. The default value is 0.
+ *
+ * @return the paint's word-spacing for drawing text.
+ * @hide
+ */
+ public float getWordSpacing() {
+ return nGetWordSpacing(mNativePaint);
+ }
+
+ /**
+ * Set the paint's word-spacing for text. The default value is 0.
+ * The value is in pixels (note the units are not the same as for
+ * letter-spacing).
+ *
+ * @param wordSpacing set the paint's word-spacing for drawing text.
+ * @hide
+ */
+ public void setWordSpacing(float wordSpacing) {
+ nSetWordSpacing(mNativePaint, wordSpacing);
+ }
+
+ /**
+ * Returns the font feature settings. The format is the same as the CSS
+ * font-feature-settings attribute:
+ * <a href="https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop">
+ * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop</a>
+ *
+ * @return the paint's currently set font feature settings. Default is null.
+ *
+ * @see #setFontFeatureSettings(String)
+ */
+ public String getFontFeatureSettings() {
+ return mFontFeatureSettings;
+ }
+
+ /**
+ * Set font feature settings.
+ *
+ * The format is the same as the CSS font-feature-settings attribute:
+ * <a href="https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop">
+ * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop</a>
+ *
+ * @see #getFontFeatureSettings()
+ *
+ * @param settings the font feature settings string to use, may be null.
+ */
+ public void setFontFeatureSettings(String settings) {
+ if (settings != null && settings.equals("")) {
+ settings = null;
+ }
+ if ((settings == null && mFontFeatureSettings == null)
+ || (settings != null && settings.equals(mFontFeatureSettings))) {
+ return;
+ }
+ mFontFeatureSettings = settings;
+ nSetFontFeatureSettings(mNativePaint, settings);
+ }
+
+ /**
+ * Returns the font variation settings.
+ *
+ * @return the paint's currently set font variation settings. Default is null.
+ *
+ * @see #setFontVariationSettings(String)
+ */
+ public String getFontVariationSettings() {
+ return mFontVariationSettings;
+ }
+
+ /**
+ * Sets TrueType or OpenType font variation settings. The settings string is constructed from
+ * multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters
+ * and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that
+ * are longer or shorter than four characters, or contain characters outside of U+0020..U+007E
+ * are invalid. If a specified axis name is not defined in the font, the settings will be
+ * ignored.
+ *
+ * Examples,
+ * <ul>
+ * <li>Set font width to 150.
+ * <pre>
+ * <code>
+ * Paint paint = new Paint();
+ * paint.setFontVariationSettings("'wdth' 150");
+ * </code>
+ * </pre>
+ * </li>
+ *
+ * <li>Set the font slant to 20 degrees and ask for italic style.
+ * <pre>
+ * <code>
+ * Paint paint = new Paint();
+ * paint.setFontVariationSettings("'slnt' 20, 'ital' 1");
+ * </code>
+ * </pre>
+ * </li>
+ * </ul>
+ *
+ * @param fontVariationSettings font variation settings. You can pass null or empty string as
+ * no variation settings.
+ *
+ * @return true if the given settings is effective to at least one font file underlying this
+ * typeface. This function also returns true for empty settings string. Otherwise
+ * returns false
+ *
+ * @throws IllegalArgumentException If given string is not a valid font variation settings
+ * format
+ *
+ * @see #getFontVariationSettings()
+ * @see FontVariationAxis
+ */
+ public boolean setFontVariationSettings(String fontVariationSettings) {
+ final String settings = TextUtils.nullIfEmpty(fontVariationSettings);
+ if (settings == mFontVariationSettings
+ || (settings != null && settings.equals(mFontVariationSettings))) {
+ return true;
+ }
+
+ if (settings == null || settings.length() == 0) {
+ mFontVariationSettings = null;
+ setTypeface(Typeface.createFromTypefaceWithVariation(mTypeface,
+ Collections.emptyList()));
+ return true;
+ }
+
+ // The null typeface is valid and it is equivalent to Typeface.DEFAULT.
+ // To call isSupportedAxes method, use Typeface.DEFAULT instance.
+ Typeface targetTypeface = mTypeface == null ? Typeface.DEFAULT : mTypeface;
+ FontVariationAxis[] axes = FontVariationAxis.fromFontVariationSettings(settings);
+ final ArrayList<FontVariationAxis> filteredAxes = new ArrayList<FontVariationAxis>();
+ for (final FontVariationAxis axis : axes) {
+ if (targetTypeface.isSupportedAxes(axis.getOpenTypeTagValue())) {
+ filteredAxes.add(axis);
+ }
+ }
+ if (filteredAxes.isEmpty()) {
+ return false;
+ }
+ mFontVariationSettings = settings;
+ setTypeface(Typeface.createFromTypefaceWithVariation(targetTypeface, filteredAxes));
+ return true;
+ }
+
+ /**
+ * Get the current value of hyphen edit.
+ *
+ * @return the current hyphen edit value
+ *
+ * @hide
+ */
+ public int getHyphenEdit() {
+ return nGetHyphenEdit(mNativePaint);
+ }
+
+ /**
+ * Set a hyphen edit on the paint (causes a hyphen to be added to text when
+ * measured or drawn).
+ *
+ * @param hyphen 0 for no edit, 1 for adding a hyphen at the end, etc.
+ * Definition of various values are in the HyphenEdit class in Minikin's Hyphenator.h.
+ *
+ * @hide
+ */
+ public void setHyphenEdit(int hyphen) {
+ nSetHyphenEdit(mNativePaint, hyphen);
+ }
+
+ /**
+ * Return the distance above (negative) the baseline (ascent) based on the
+ * current typeface and text size.
+ *
+ * <p>Note that this is the ascent of the main typeface, and actual text rendered may need a
+ * larger ascent because fallback fonts may get used in rendering the text.
+ *
+ * @return the distance above (negative) the baseline (ascent) based on the
+ * current typeface and text size.
+ */
+ public float ascent() {
+ return nAscent(mNativePaint);
+ }
+
+ /**
+ * Return the distance below (positive) the baseline (descent) based on the
+ * current typeface and text size.
+ *
+ * <p>Note that this is the descent of the main typeface, and actual text rendered may need a
+ * larger descent because fallback fonts may get used in rendering the text.
+ *
+ * @return the distance below (positive) the baseline (descent) based on
+ * the current typeface and text size.
+ */
+ public float descent() {
+ return nDescent(mNativePaint);
+ }
+
+ /**
+ * Class that describes the various metrics for a font at a given text size.
+ * Remember, Y values increase going down, so those values will be positive,
+ * and values that measure distances going up will be negative. This class
+ * is returned by getFontMetrics().
+ */
+ public static class FontMetrics {
+ /**
+ * The maximum distance above the baseline for the tallest glyph in
+ * the font at a given text size.
+ */
+ public float top;
+ /**
+ * The recommended distance above the baseline for singled spaced text.
+ */
+ public float ascent;
+ /**
+ * The recommended distance below the baseline for singled spaced text.
+ */
+ public float descent;
+ /**
+ * The maximum distance below the baseline for the lowest glyph in
+ * the font at a given text size.
+ */
+ public float bottom;
+ /**
+ * The recommended additional space to add between lines of text.
+ */
+ public float leading;
+ }
+
+ /**
+ * Return the font's recommended interline spacing, given the Paint's
+ * settings for typeface, textSize, etc. If metrics is not null, return the
+ * fontmetric values in it.
+ *
+ * <p>Note that these are the values for the main typeface, and actual text rendered may need a
+ * larger set of values because fallback fonts may get used in rendering the text.
+ *
+ * @param metrics If this object is not null, its fields are filled with
+ * the appropriate values given the paint's text attributes.
+ * @return the font's recommended interline spacing.
+ */
+ public float getFontMetrics(FontMetrics metrics) {
+ return nGetFontMetrics(mNativePaint, metrics);
+ }
+
+ /**
+ * Allocates a new FontMetrics object, and then calls getFontMetrics(fm)
+ * with it, returning the object.
+ */
+ public FontMetrics getFontMetrics() {
+ FontMetrics fm = new FontMetrics();
+ getFontMetrics(fm);
+ return fm;
+ }
+
+ /**
+ * Convenience method for callers that want to have FontMetrics values as
+ * integers.
+ */
+ public static class FontMetricsInt {
+ /**
+ * The maximum distance above the baseline for the tallest glyph in
+ * the font at a given text size.
+ */
+ public int top;
+ /**
+ * The recommended distance above the baseline for singled spaced text.
+ */
+ public int ascent;
+ /**
+ * The recommended distance below the baseline for singled spaced text.
+ */
+ public int descent;
+ /**
+ * The maximum distance below the baseline for the lowest glyph in
+ * the font at a given text size.
+ */
+ public int bottom;
+ /**
+ * The recommended additional space to add between lines of text.
+ */
+ public int leading;
+
+ @Override public String toString() {
+ return "FontMetricsInt: top=" + top + " ascent=" + ascent +
+ " descent=" + descent + " bottom=" + bottom +
+ " leading=" + leading;
+ }
+ }
+
+ /**
+ * Return the font's interline spacing, given the Paint's settings for
+ * typeface, textSize, etc. If metrics is not null, return the fontmetric
+ * values in it. Note: all values have been converted to integers from
+ * floats, in such a way has to make the answers useful for both spacing
+ * and clipping. If you want more control over the rounding, call
+ * getFontMetrics().
+ *
+ * <p>Note that these are the values for the main typeface, and actual text rendered may need a
+ * larger set of values because fallback fonts may get used in rendering the text.
+ *
+ * @return the font's interline spacing.
+ */
+ public int getFontMetricsInt(FontMetricsInt fmi) {
+ return nGetFontMetricsInt(mNativePaint, fmi);
+ }
+
+ public FontMetricsInt getFontMetricsInt() {
+ FontMetricsInt fm = new FontMetricsInt();
+ getFontMetricsInt(fm);
+ return fm;
+ }
+
+ /**
+ * Return the recommend line spacing based on the current typeface and
+ * text size.
+ *
+ * <p>Note that this is the value for the main typeface, and actual text rendered may need a
+ * larger value because fallback fonts may get used in rendering the text.
+ *
+ * @return recommend line spacing based on the current typeface and
+ * text size.
+ */
+ public float getFontSpacing() {
+ return getFontMetrics(null);
+ }
+
+ /**
+ * Return the width of the text.
+ *
+ * @param text The text to measure. Cannot be null.
+ * @param index The index of the first character to start measuring
+ * @param count THe number of characters to measure, beginning with start
+ * @return The width of the text
+ */
+ public float measureText(char[] text, int index, int count) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((index | count) < 0 || index + count > text.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ if (text.length == 0 || count == 0) {
+ return 0f;
+ }
+ if (!mHasCompatScaling) {
+ return (float) Math.ceil(nGetTextAdvances(mNativePaint, text,
+ index, count, index, count, mBidiFlags, null, 0));
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ final float w = nGetTextAdvances(mNativePaint, text, index, count, index, count,
+ mBidiFlags, null, 0);
+ setTextSize(oldSize);
+ return (float) Math.ceil(w*mInvCompatScaling);
+ }
+
+ /**
+ * Return the width of the text.
+ *
+ * @param text The text to measure. Cannot be null.
+ * @param start The index of the first character to start measuring
+ * @param end 1 beyond the index of the last character to measure
+ * @return The width of the text
+ */
+ public float measureText(String text, int start, int end) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0f;
+ }
+ if (!mHasCompatScaling) {
+ return (float) Math.ceil(nGetTextAdvances(mNativePaint, text,
+ start, end, start, end, mBidiFlags, null, 0));
+ }
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ final float w = nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags,
+ null, 0);
+ setTextSize(oldSize);
+ return (float) Math.ceil(w * mInvCompatScaling);
+ }
+
+ /**
+ * Return the width of the text.
+ *
+ * @param text The text to measure. Cannot be null.
+ * @return The width of the text
+ */
+ public float measureText(String text) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ return measureText(text, 0, text.length());
+ }
+
+ /**
+ * Return the width of the text.
+ *
+ * @param text The text to measure
+ * @param start The index of the first character to start measuring
+ * @param end 1 beyond the index of the last character to measure
+ * @return The width of the text
+ */
+ public float measureText(CharSequence text, int start, int end) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0f;
+ }
+ if (text instanceof String) {
+ return measureText((String)text, start, end);
+ }
+ if (text instanceof SpannedString ||
+ text instanceof SpannableString) {
+ return measureText(text.toString(), start, end);
+ }
+ if (text instanceof GraphicsOperations) {
+ return ((GraphicsOperations)text).measureText(start, end, this);
+ }
+
+ char[] buf = TemporaryBuffer.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ float result = measureText(buf, 0, end - start);
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ /**
+ * Measure the text, stopping early if the measured width exceeds maxWidth.
+ * Return the number of chars that were measured, and if measuredWidth is
+ * not null, return in it the actual width measured.
+ *
+ * @param text The text to measure. Cannot be null.
+ * @param index The offset into text to begin measuring at
+ * @param count The number of maximum number of entries to measure. If count
+ * is negative, then the characters are measured in reverse order.
+ * @param maxWidth The maximum width to accumulate.
+ * @param measuredWidth Optional. If not null, returns the actual width
+ * measured.
+ * @return The number of chars that were measured. Will always be <=
+ * abs(count).
+ */
+ public int breakText(char[] text, int index, int count,
+ float maxWidth, float[] measuredWidth) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if (index < 0 || text.length - index < Math.abs(count)) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ if (text.length == 0 || count == 0) {
+ return 0;
+ }
+ if (!mHasCompatScaling) {
+ return nBreakText(mNativePaint, text, index, count, maxWidth, mBidiFlags,
+ measuredWidth);
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ final int res = nBreakText(mNativePaint, text, index, count, maxWidth * mCompatScaling,
+ mBidiFlags, measuredWidth);
+ setTextSize(oldSize);
+ if (measuredWidth != null) measuredWidth[0] *= mInvCompatScaling;
+ return res;
+ }
+
+ /**
+ * Measure the text, stopping early if the measured width exceeds maxWidth.
+ * Return the number of chars that were measured, and if measuredWidth is
+ * not null, return in it the actual width measured.
+ *
+ * @param text The text to measure. Cannot be null.
+ * @param start The offset into text to begin measuring at
+ * @param end The end of the text slice to measure.
+ * @param measureForwards If true, measure forwards, starting at start.
+ * Otherwise, measure backwards, starting with end.
+ * @param maxWidth The maximum width to accumulate.
+ * @param measuredWidth Optional. If not null, returns the actual width
+ * measured.
+ * @return The number of chars that were measured. Will always be <=
+ * abs(end - start).
+ */
+ public int breakText(CharSequence text, int start, int end,
+ boolean measureForwards,
+ float maxWidth, float[] measuredWidth) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0;
+ }
+ if (start == 0 && text instanceof String && end == text.length()) {
+ return breakText((String) text, measureForwards, maxWidth,
+ measuredWidth);
+ }
+
+ char[] buf = TemporaryBuffer.obtain(end - start);
+ int result;
+
+ TextUtils.getChars(text, start, end, buf, 0);
+
+ if (measureForwards) {
+ result = breakText(buf, 0, end - start, maxWidth, measuredWidth);
+ } else {
+ result = breakText(buf, 0, -(end - start), maxWidth, measuredWidth);
+ }
+
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ /**
+ * Measure the text, stopping early if the measured width exceeds maxWidth.
+ * Return the number of chars that were measured, and if measuredWidth is
+ * not null, return in it the actual width measured.
+ *
+ * @param text The text to measure. Cannot be null.
+ * @param measureForwards If true, measure forwards, starting with the
+ * first character in the string. Otherwise,
+ * measure backwards, starting with the
+ * last character in the string.
+ * @param maxWidth The maximum width to accumulate.
+ * @param measuredWidth Optional. If not null, returns the actual width
+ * measured.
+ * @return The number of chars that were measured. Will always be <=
+ * abs(count).
+ */
+ public int breakText(String text, boolean measureForwards,
+ float maxWidth, float[] measuredWidth) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+
+ if (text.length() == 0) {
+ return 0;
+ }
+ if (!mHasCompatScaling) {
+ return nBreakText(mNativePaint, text, measureForwards,
+ maxWidth, mBidiFlags, measuredWidth);
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize*mCompatScaling);
+ final int res = nBreakText(mNativePaint, text, measureForwards, maxWidth*mCompatScaling,
+ mBidiFlags, measuredWidth);
+ setTextSize(oldSize);
+ if (measuredWidth != null) measuredWidth[0] *= mInvCompatScaling;
+ return res;
+ }
+
+ /**
+ * Return the advance widths for the characters in the string.
+ *
+ * @param text The text to measure. Cannot be null.
+ * @param index The index of the first char to to measure
+ * @param count The number of chars starting with index to measure
+ * @param widths array to receive the advance widths of the characters.
+ * Must be at least a large as count.
+ * @return the actual number of widths returned.
+ */
+ public int getTextWidths(char[] text, int index, int count,
+ float[] widths) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((index | count) < 0 || index + count > text.length
+ || count > widths.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ if (text.length == 0 || count == 0) {
+ return 0;
+ }
+ if (!mHasCompatScaling) {
+ nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, widths, 0);
+ return count;
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, widths, 0);
+ setTextSize(oldSize);
+ for (int i = 0; i < count; i++) {
+ widths[i] *= mInvCompatScaling;
+ }
+ return count;
+ }
+
+ /**
+ * Return the advance widths for the characters in the string.
+ *
+ * @param text The text to measure. Cannot be null.
+ * @param start The index of the first char to to measure
+ * @param end The end of the text slice to measure
+ * @param widths array to receive the advance widths of the characters.
+ * Must be at least a large as (end - start).
+ * @return the actual number of widths returned.
+ */
+ public int getTextWidths(CharSequence text, int start, int end,
+ float[] widths) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (end - start > widths.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0;
+ }
+ if (text instanceof String) {
+ return getTextWidths((String) text, start, end, widths);
+ }
+ if (text instanceof SpannedString ||
+ text instanceof SpannableString) {
+ return getTextWidths(text.toString(), start, end, widths);
+ }
+ if (text instanceof GraphicsOperations) {
+ return ((GraphicsOperations) text).getTextWidths(start, end,
+ widths, this);
+ }
+
+ char[] buf = TemporaryBuffer.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ int result = getTextWidths(buf, 0, end - start, widths);
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ /**
+ * Return the advance widths for the characters in the string.
+ *
+ * @param text The text to measure. Cannot be null.
+ * @param start The index of the first char to to measure
+ * @param end The end of the text slice to measure
+ * @param widths array to receive the advance widths of the characters.
+ * Must be at least a large as the text.
+ * @return the number of code units in the specified text.
+ */
+ public int getTextWidths(String text, int start, int end, float[] widths) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (end - start > widths.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0;
+ }
+ if (!mHasCompatScaling) {
+ nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, widths, 0);
+ return end - start;
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, widths, 0);
+ setTextSize(oldSize);
+ for (int i = 0; i < end - start; i++) {
+ widths[i] *= mInvCompatScaling;
+ }
+ return end - start;
+ }
+
+ /**
+ * Return the advance widths for the characters in the string.
+ *
+ * @param text The text to measure
+ * @param widths array to receive the advance widths of the characters.
+ * Must be at least a large as the text.
+ * @return the number of code units in the specified text.
+ */
+ public int getTextWidths(String text, float[] widths) {
+ return getTextWidths(text, 0, text.length(), widths);
+ }
+
+ /**
+ * Convenience overload that takes a char array instead of a
+ * String.
+ *
+ * @see #getTextRunAdvances(String, int, int, int, int, boolean, float[], int)
+ * @hide
+ */
+ public float getTextRunAdvances(char[] chars, int index, int count,
+ int contextIndex, int contextCount, boolean isRtl, float[] advances,
+ int advancesIndex) {
+
+ if (chars == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((index | count | contextIndex | contextCount | advancesIndex
+ | (index - contextIndex) | (contextCount - count)
+ | ((contextIndex + contextCount) - (index + count))
+ | (chars.length - (contextIndex + contextCount))
+ | (advances == null ? 0 :
+ (advances.length - (advancesIndex + count)))) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (chars.length == 0 || count == 0){
+ return 0f;
+ }
+ if (!mHasCompatScaling) {
+ return nGetTextAdvances(mNativePaint, chars, index, count, contextIndex, contextCount,
+ isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances,
+ advancesIndex);
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ final float res = nGetTextAdvances(mNativePaint, chars, index, count, contextIndex,
+ contextCount, isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, advancesIndex);
+ setTextSize(oldSize);
+
+ if (advances != null) {
+ for (int i = advancesIndex, e = i + count; i < e; i++) {
+ advances[i] *= mInvCompatScaling;
+ }
+ }
+ return res * mInvCompatScaling; // assume errors are not significant
+ }
+
+ /**
+ * Convenience overload that takes a CharSequence instead of a
+ * String.
+ *
+ * @see #getTextRunAdvances(String, int, int, int, int, boolean, float[], int)
+ * @hide
+ */
+ public float getTextRunAdvances(CharSequence text, int start, int end,
+ int contextStart, int contextEnd, boolean isRtl, float[] advances,
+ int advancesIndex) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | contextStart | contextEnd | advancesIndex | (end - start)
+ | (start - contextStart) | (contextEnd - end)
+ | (text.length() - contextEnd)
+ | (advances == null ? 0 :
+ (advances.length - advancesIndex - (end - start)))) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (text instanceof String) {
+ return getTextRunAdvances((String) text, start, end,
+ contextStart, contextEnd, isRtl, advances, advancesIndex);
+ }
+ if (text instanceof SpannedString ||
+ text instanceof SpannableString) {
+ return getTextRunAdvances(text.toString(), start, end,
+ contextStart, contextEnd, isRtl, advances, advancesIndex);
+ }
+ if (text instanceof GraphicsOperations) {
+ return ((GraphicsOperations) text).getTextRunAdvances(start, end,
+ contextStart, contextEnd, isRtl, advances, advancesIndex, this);
+ }
+ if (text.length() == 0 || end == start) {
+ return 0f;
+ }
+
+ int contextLen = contextEnd - contextStart;
+ int len = end - start;
+ char[] buf = TemporaryBuffer.obtain(contextLen);
+ TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ float result = getTextRunAdvances(buf, start - contextStart, len,
+ 0, contextLen, isRtl, advances, advancesIndex);
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ /**
+ * Returns the total advance width for the characters in the run
+ * between start and end, and if advances is not null, the advance
+ * assigned to each of these characters (java chars).
+ *
+ * <p>The trailing surrogate in a valid surrogate pair is assigned
+ * an advance of 0. Thus the number of returned advances is
+ * always equal to count, not to the number of unicode codepoints
+ * represented by the run.
+ *
+ * <p>In the case of conjuncts or combining marks, the total
+ * advance is assigned to the first logical character, and the
+ * following characters are assigned an advance of 0.
+ *
+ * <p>This generates the sum of the advances of glyphs for
+ * characters in a reordered cluster as the width of the first
+ * logical character in the cluster, and 0 for the widths of all
+ * other characters in the cluster. In effect, such clusters are
+ * treated like conjuncts.
+ *
+ * <p>The shaping bounds limit the amount of context available
+ * outside start and end that can be used for shaping analysis.
+ * These bounds typically reflect changes in bidi level or font
+ * metrics across which shaping does not occur.
+ *
+ * @param text the text to measure. Cannot be null.
+ * @param start the index of the first character to measure
+ * @param end the index past the last character to measure
+ * @param contextStart the index of the first character to use for shaping context,
+ * must be <= start
+ * @param contextEnd the index past the last character to use for shaping context,
+ * must be >= end
+ * @param isRtl whether the run is in RTL direction
+ * @param advances array to receive the advances, must have room for all advances,
+ * can be null if only total advance is needed
+ * @param advancesIndex the position in advances at which to put the
+ * advance corresponding to the character at start
+ * @return the total advance
+ *
+ * @hide
+ */
+ public float getTextRunAdvances(String text, int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, float[] advances, int advancesIndex) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | contextStart | contextEnd | advancesIndex | (end - start)
+ | (start - contextStart) | (contextEnd - end)
+ | (text.length() - contextEnd)
+ | (advances == null ? 0 :
+ (advances.length - advancesIndex - (end - start)))) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0f;
+ }
+
+ if (!mHasCompatScaling) {
+ return nGetTextAdvances(mNativePaint, text, start, end, contextStart, contextEnd,
+ isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, advancesIndex);
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ final float totalAdvance = nGetTextAdvances(mNativePaint, text, start, end, contextStart,
+ contextEnd, isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, advancesIndex);
+ setTextSize(oldSize);
+
+ if (advances != null) {
+ for (int i = advancesIndex, e = i + (end - start); i < e; i++) {
+ advances[i] *= mInvCompatScaling;
+ }
+ }
+ return totalAdvance * mInvCompatScaling; // assume errors are insignificant
+ }
+
+ /**
+ * Returns the next cursor position in the run. This avoids placing the
+ * cursor between surrogates, between characters that form conjuncts,
+ * between base characters and combining marks, or within a reordering
+ * cluster.
+ *
+ * <p>ContextStart and offset are relative to the start of text.
+ * The context is the shaping context for cursor movement, generally
+ * the bounds of the metric span enclosing the cursor in the direction of
+ * movement.
+ *
+ * <p>If cursorOpt is {@link #CURSOR_AT} and the offset is not a valid
+ * cursor position, this returns -1. Otherwise this will never return a
+ * value before contextStart or after contextStart + contextLength.
+ *
+ * @param text the text
+ * @param contextStart the start of the context
+ * @param contextLength the length of the context
+ * @param dir either {@link #DIRECTION_RTL} or {@link #DIRECTION_LTR}
+ * @param offset the cursor position to move from
+ * @param cursorOpt how to move the cursor, one of {@link #CURSOR_AFTER},
+ * {@link #CURSOR_AT_OR_AFTER}, {@link #CURSOR_BEFORE},
+ * {@link #CURSOR_AT_OR_BEFORE}, or {@link #CURSOR_AT}
+ * @return the offset of the next position, or -1
+ * @hide
+ */
+ public int getTextRunCursor(char[] text, int contextStart, int contextLength,
+ int dir, int offset, int cursorOpt) {
+ int contextEnd = contextStart + contextLength;
+ if (((contextStart | contextEnd | offset | (contextEnd - contextStart)
+ | (offset - contextStart) | (contextEnd - offset)
+ | (text.length - contextEnd) | cursorOpt) < 0)
+ || cursorOpt > CURSOR_OPT_MAX_VALUE) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ return nGetTextRunCursor(mNativePaint, text, contextStart, contextLength, dir, offset,
+ cursorOpt);
+ }
+
+ /**
+ * Returns the next cursor position in the run. This avoids placing the
+ * cursor between surrogates, between characters that form conjuncts,
+ * between base characters and combining marks, or within a reordering
+ * cluster.
+ *
+ * <p>ContextStart, contextEnd, and offset are relative to the start of
+ * text. The context is the shaping context for cursor movement, generally
+ * the bounds of the metric span enclosing the cursor in the direction of
+ * movement.
+ *
+ * <p>If cursorOpt is {@link #CURSOR_AT} and the offset is not a valid
+ * cursor position, this returns -1. Otherwise this will never return a
+ * value before contextStart or after contextEnd.
+ *
+ * @param text the text
+ * @param contextStart the start of the context
+ * @param contextEnd the end of the context
+ * @param dir either {@link #DIRECTION_RTL} or {@link #DIRECTION_LTR}
+ * @param offset the cursor position to move from
+ * @param cursorOpt how to move the cursor, one of {@link #CURSOR_AFTER},
+ * {@link #CURSOR_AT_OR_AFTER}, {@link #CURSOR_BEFORE},
+ * {@link #CURSOR_AT_OR_BEFORE}, or {@link #CURSOR_AT}
+ * @return the offset of the next position, or -1
+ * @hide
+ */
+ public int getTextRunCursor(CharSequence text, int contextStart,
+ int contextEnd, int dir, int offset, int cursorOpt) {
+
+ if (text instanceof String || text instanceof SpannedString ||
+ text instanceof SpannableString) {
+ return getTextRunCursor(text.toString(), contextStart, contextEnd,
+ dir, offset, cursorOpt);
+ }
+ if (text instanceof GraphicsOperations) {
+ return ((GraphicsOperations) text).getTextRunCursor(
+ contextStart, contextEnd, dir, offset, cursorOpt, this);
+ }
+
+ int contextLen = contextEnd - contextStart;
+ char[] buf = TemporaryBuffer.obtain(contextLen);
+ TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ int relPos = getTextRunCursor(buf, 0, contextLen, dir, offset - contextStart, cursorOpt);
+ TemporaryBuffer.recycle(buf);
+ return (relPos == -1) ? -1 : relPos + contextStart;
+ }
+
+ /**
+ * Returns the next cursor position in the run. This avoids placing the
+ * cursor between surrogates, between characters that form conjuncts,
+ * between base characters and combining marks, or within a reordering
+ * cluster.
+ *
+ * <p>ContextStart, contextEnd, and offset are relative to the start of
+ * text. The context is the shaping context for cursor movement, generally
+ * the bounds of the metric span enclosing the cursor in the direction of
+ * movement.
+ *
+ * <p>If cursorOpt is {@link #CURSOR_AT} and the offset is not a valid
+ * cursor position, this returns -1. Otherwise this will never return a
+ * value before contextStart or after contextEnd.
+ *
+ * @param text the text
+ * @param contextStart the start of the context
+ * @param contextEnd the end of the context
+ * @param dir either {@link #DIRECTION_RTL} or {@link #DIRECTION_LTR}
+ * @param offset the cursor position to move from
+ * @param cursorOpt how to move the cursor, one of {@link #CURSOR_AFTER},
+ * {@link #CURSOR_AT_OR_AFTER}, {@link #CURSOR_BEFORE},
+ * {@link #CURSOR_AT_OR_BEFORE}, or {@link #CURSOR_AT}
+ * @return the offset of the next position, or -1
+ * @hide
+ */
+ public int getTextRunCursor(String text, int contextStart, int contextEnd,
+ int dir, int offset, int cursorOpt) {
+ if (((contextStart | contextEnd | offset | (contextEnd - contextStart)
+ | (offset - contextStart) | (contextEnd - offset)
+ | (text.length() - contextEnd) | cursorOpt) < 0)
+ || cursorOpt > CURSOR_OPT_MAX_VALUE) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ return nGetTextRunCursor(mNativePaint, text, contextStart, contextEnd, dir, offset,
+ cursorOpt);
+ }
+
+ /**
+ * Return the path (outline) for the specified text.
+ * Note: just like Canvas.drawText, this will respect the Align setting in
+ * the paint.
+ *
+ * @param text the text to retrieve the path from
+ * @param index the index of the first character in text
+ * @param count the number of characters starting with index
+ * @param x the x coordinate of the text's origin
+ * @param y the y coordinate of the text's origin
+ * @param path the path to receive the data describing the text. Must be allocated by the caller
+ */
+ public void getTextPath(char[] text, int index, int count,
+ float x, float y, Path path) {
+ if ((index | count) < 0 || index + count > text.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ nGetTextPath(mNativePaint, mBidiFlags, text, index, count, x, y, path.mutateNI());
+ }
+
+ /**
+ * Return the path (outline) for the specified text.
+ * Note: just like Canvas.drawText, this will respect the Align setting
+ * in the paint.
+ *
+ * @param text the text to retrieve the path from
+ * @param start the first character in the text
+ * @param end 1 past the last character in the text
+ * @param x the x coordinate of the text's origin
+ * @param y the y coordinate of the text's origin
+ * @param path the path to receive the data describing the text. Must be allocated by the caller
+ */
+ public void getTextPath(String text, int start, int end,
+ float x, float y, Path path) {
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ nGetTextPath(mNativePaint, mBidiFlags, text, start, end, x, y, path.mutateNI());
+ }
+
+ /**
+ * Return in bounds (allocated by the caller) the smallest rectangle that
+ * encloses all of the characters, with an implied origin at (0,0).
+ *
+ * @param text string to measure and return its bounds
+ * @param start index of the first char in the string to measure
+ * @param end 1 past the last char in the string to measure
+ * @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
+ */
+ public void getTextBounds(String text, int start, int end, Rect bounds) {
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (bounds == null) {
+ throw new NullPointerException("need bounds Rect");
+ }
+ nGetStringBounds(mNativePaint, text, start, end, mBidiFlags, bounds);
+ }
+
+ /**
+ * Return in bounds (allocated by the caller) the smallest rectangle that
+ * encloses all of the characters, with an implied origin at (0,0).
+ *
+ * @param text text to measure and return its bounds
+ * @param start index of the first char in the text to measure
+ * @param end 1 past the last char in the text to measure
+ * @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
+ * @hide
+ */
+ public void getTextBounds(CharSequence text, int start, int end, Rect bounds) {
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (bounds == null) {
+ throw new NullPointerException("need bounds Rect");
+ }
+ char[] buf = TemporaryBuffer.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ getTextBounds(buf, 0, end - start, bounds);
+ TemporaryBuffer.recycle(buf);
+ }
+
+ /**
+ * Return in bounds (allocated by the caller) the smallest rectangle that
+ * encloses all of the characters, with an implied origin at (0,0).
+ *
+ * @param text array of chars to measure and return their unioned bounds
+ * @param index index of the first char in the array to measure
+ * @param count the number of chars, beginning at index, to measure
+ * @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
+ */
+ public void getTextBounds(char[] text, int index, int count, Rect bounds) {
+ if ((index | count) < 0 || index + count > text.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ if (bounds == null) {
+ throw new NullPointerException("need bounds Rect");
+ }
+ nGetCharArrayBounds(mNativePaint, text, index, count, mBidiFlags,
+ bounds);
+ }
+
+ /**
+ * Determine whether the typeface set on the paint has a glyph supporting the string. The
+ * simplest case is when the string contains a single character, in which this method
+ * determines whether the font has the character. In the case of multiple characters, the
+ * method returns true if there is a single glyph representing the ligature. For example, if
+ * the input is a pair of regional indicator symbols, determine whether there is an emoji flag
+ * for the pair.
+ *
+ * <p>Finally, if the string contains a variation selector, the method only returns true if
+ * the fonts contains a glyph specific to that variation.
+ *
+ * <p>Checking is done on the entire fallback chain, not just the immediate font referenced.
+ *
+ * @param string the string to test whether there is glyph support
+ * @return true if the typeface has a glyph for the string
+ */
+ public boolean hasGlyph(String string) {
+ return nHasGlyph(mNativePaint, mBidiFlags, string);
+ }
+
+ /**
+ * Measure cursor position within a run of text.
+ *
+ * <p>The run of text includes the characters from {@code start} to {@code end} in the text. In
+ * addition, the range {@code contextStart} to {@code contextEnd} is used as context for the
+ * purpose of complex text shaping, such as Arabic text potentially shaped differently based on
+ * the text next to it.
+ *
+ * <p>All text outside the range {@code contextStart..contextEnd} is ignored. The text between
+ * {@code start} and {@code end} will be laid out to be measured.
+ *
+ * <p>The returned width measurement is the advance from {@code start} to {@code offset}. It is
+ * generally a positive value, no matter the direction of the run. If {@code offset == end},
+ * the return value is simply the width of the whole run from {@code start} to {@code end}.
+ *
+ * <p>Ligatures are formed for characters in the range {@code start..end} (but not for
+ * {@code start..contextStart} or {@code end..contextEnd}). If {@code offset} points to a
+ * character in the middle of such a formed ligature, but at a grapheme cluster boundary, the
+ * return value will also reflect an advance in the middle of the ligature. See
+ * {@link #getOffsetForAdvance} for more discussion of grapheme cluster boundaries.
+ *
+ * <p>The direction of the run is explicitly specified by {@code isRtl}. Thus, this method is
+ * suitable only for runs of a single direction.
+ *
+ * <p>All indices are relative to the start of {@code text}. Further, {@code 0 <= contextStart
+ * <= start <= offset <= end <= contextEnd <= text.length} must hold on entry.
+ *
+ * @param text the text to measure. Cannot be null.
+ * @param start the index of the start of the range to measure
+ * @param end the index + 1 of the end of the range to measure
+ * @param contextStart the index of the start of the shaping context
+ * @param contextEnd the index + 1 of the end of the shaping context
+ * @param isRtl whether the run is in RTL direction
+ * @param offset index of caret position
+ * @return width measurement between start and offset
+ */
+ public float getRunAdvance(char[] text, int start, int end, int contextStart, int contextEnd,
+ boolean isRtl, int offset) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((contextStart | start | offset | end | contextEnd
+ | start - contextStart | offset - start | end - offset
+ | contextEnd - end | text.length - contextEnd) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (end == start) {
+ return 0.0f;
+ }
+ // TODO: take mCompatScaling into account (or eliminate compat scaling)?
+ return nGetRunAdvance(mNativePaint, text, start, end, contextStart, contextEnd, isRtl,
+ offset);
+ }
+
+ /**
+ * @see #getRunAdvance(char[], int, int, int, int, boolean, int)
+ *
+ * @param text the text to measure. Cannot be null.
+ * @param start the index of the start of the range to measure
+ * @param end the index + 1 of the end of the range to measure
+ * @param contextStart the index of the start of the shaping context
+ * @param contextEnd the index + 1 of the end of the shaping context
+ * @param isRtl whether the run is in RTL direction
+ * @param offset index of caret position
+ * @return width measurement between start and offset
+ */
+ public float getRunAdvance(@NonNull CharSequence text, int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, int offset) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((contextStart | start | offset | end | contextEnd
+ | start - contextStart | offset - start | end - offset
+ | contextEnd - end | text.length() - contextEnd) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (end == start) {
+ return 0.0f;
+ }
+ // TODO performance: specialized alternatives to avoid buffer copy, if win is significant
+ char[] buf = TemporaryBuffer.obtain(contextEnd - contextStart);
+ TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ float result = getRunAdvance(buf, start - contextStart, end - contextStart, 0,
+ contextEnd - contextStart, isRtl, offset - contextStart);
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ /**
+ * Get the character offset within the string whose position is closest to the specified
+ * horizontal position.
+ *
+ * <p>The returned value is generally the value of {@code offset} for which
+ * {@link #getRunAdvance} yields a result most closely approximating {@code advance},
+ * and which is also on a grapheme cluster boundary. As such, it is the preferred method
+ * for positioning a cursor in response to a touch or pointer event. The grapheme cluster
+ * boundaries are based on
+ * <a href="http://unicode.org/reports/tr29/">Unicode Standard Annex #29</a> but with some
+ * tailoring for better user experience.
+ *
+ * <p>Note that {@code advance} is a (generally positive) width measurement relative to the start
+ * of the run. Thus, for RTL runs it the distance from the point to the right edge.
+ *
+ * <p>All indices are relative to the start of {@code text}. Further, {@code 0 <= contextStart
+ * <= start <= end <= contextEnd <= text.length} must hold on entry, and {@code start <= result
+ * <= end} will hold on return.
+ *
+ * @param text the text to measure. Cannot be null.
+ * @param start the index of the start of the range to measure
+ * @param end the index + 1 of the end of the range to measure
+ * @param contextStart the index of the start of the shaping context
+ * @param contextEnd the index + 1 of the end of the range to measure
+ * @param isRtl whether the run is in RTL direction
+ * @param advance width relative to start of run
+ * @return index of offset
+ */
+ public int getOffsetForAdvance(char[] text, int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, float advance) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((contextStart | start | end | contextEnd
+ | start - contextStart | end - start | contextEnd - end
+ | text.length - contextEnd) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ // TODO: take mCompatScaling into account (or eliminate compat scaling)?
+ return nGetOffsetForAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
+ isRtl, advance);
+ }
+
+ /**
+ * @see #getOffsetForAdvance(char[], int, int, int, int, boolean, float)
+ *
+ * @param text the text to measure. Cannot be null.
+ * @param start the index of the start of the range to measure
+ * @param end the index + 1 of the end of the range to measure
+ * @param contextStart the index of the start of the shaping context
+ * @param contextEnd the index + 1 of the end of the range to measure
+ * @param isRtl whether the run is in RTL direction
+ * @param advance width relative to start of run
+ * @return index of offset
+ */
+ public int getOffsetForAdvance(CharSequence text, int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, float advance) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((contextStart | start | end | contextEnd
+ | start - contextStart | end - start | contextEnd - end
+ | text.length() - contextEnd) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ // TODO performance: specialized alternatives to avoid buffer copy, if win is significant
+ char[] buf = TemporaryBuffer.obtain(contextEnd - contextStart);
+ TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ int result = getOffsetForAdvance(buf, start - contextStart, end - contextStart, 0,
+ contextEnd - contextStart, isRtl, advance) + contextStart;
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ // regular JNI
+ private static native long nGetNativeFinalizer();
+ private static native long nInit();
+ private static native long nInitWithPaint(long paint);
+ private static native int nBreakText(long nObject, char[] text, int index, int count,
+ float maxWidth, int bidiFlags, float[] measuredWidth);
+ private static native int nBreakText(long nObject, String text, boolean measureForwards,
+ float maxWidth, int bidiFlags, float[] measuredWidth);
+ private static native float nGetTextAdvances(long paintPtr, char[] text, int index, int count,
+ int contextIndex, int contextCount, int bidiFlags, float[] advances, int advancesIndex);
+ private static native float nGetTextAdvances(long paintPtr, String text, int start, int end,
+ int contextStart, int contextEnd, int bidiFlags, float[] advances, int advancesIndex);
+ private native int nGetTextRunCursor(long paintPtr, char[] text, int contextStart,
+ int contextLength, int dir, int offset, int cursorOpt);
+ private native int nGetTextRunCursor(long paintPtr, String text, int contextStart,
+ int contextEnd, int dir, int offset, int cursorOpt);
+ private static native void nGetTextPath(long paintPtr, int bidiFlags, char[] text, int index,
+ int count, float x, float y, long path);
+ private static native void nGetTextPath(long paintPtr, int bidiFlags, String text, int start,
+ int end, float x, float y, long path);
+ private static native void nGetStringBounds(long nativePaint, String text, int start, int end,
+ int bidiFlags, Rect bounds);
+ private static native void nGetCharArrayBounds(long nativePaint, char[] text, int index,
+ int count, int bidiFlags, Rect bounds);
+ private static native boolean nHasGlyph(long paintPtr, int bidiFlags, String string);
+ private static native float nGetRunAdvance(long paintPtr, char[] text, int start, int end,
+ int contextStart, int contextEnd, boolean isRtl, int offset);
+ private static native int nGetOffsetForAdvance(long paintPtr, char[] text, int start, int end,
+ int contextStart, int contextEnd, boolean isRtl, float advance);
+
+
+ // ---------------- @FastNative ------------------------
+
+ @FastNative
+ private static native int nSetTextLocales(long paintPtr, String locales);
+ @FastNative
+ private static native void nSetFontFeatureSettings(long paintPtr, String settings);
+ @FastNative
+ private static native float nGetFontMetrics(long paintPtr, FontMetrics metrics);
+ @FastNative
+ private static native int nGetFontMetricsInt(long paintPtr, FontMetricsInt fmi);
+
+
+ // ---------------- @CriticalNative ------------------------
+
+ @CriticalNative
+ private static native void nReset(long paintPtr);
+ @CriticalNative
+ private static native void nSet(long paintPtrDest, long paintPtrSrc);
+ @CriticalNative
+ private static native int nGetStyle(long paintPtr);
+ @CriticalNative
+ private static native void nSetStyle(long paintPtr, int style);
+ @CriticalNative
+ private static native int nGetStrokeCap(long paintPtr);
+ @CriticalNative
+ private static native void nSetStrokeCap(long paintPtr, int cap);
+ @CriticalNative
+ private static native int nGetStrokeJoin(long paintPtr);
+ @CriticalNative
+ private static native void nSetStrokeJoin(long paintPtr, int join);
+ @CriticalNative
+ private static native boolean nGetFillPath(long paintPtr, long src, long dst);
+ @CriticalNative
+ private static native long nSetShader(long paintPtr, long shader);
+ @CriticalNative
+ private static native long nSetColorFilter(long paintPtr, long filter);
+ @CriticalNative
+ private static native void nSetXfermode(long paintPtr, int xfermode);
+ @CriticalNative
+ private static native long nSetPathEffect(long paintPtr, long effect);
+ @CriticalNative
+ private static native long nSetMaskFilter(long paintPtr, long maskfilter);
+ @CriticalNative
+ private static native void nSetTypeface(long paintPtr, long typeface);
+ @CriticalNative
+ private static native int nGetTextAlign(long paintPtr);
+ @CriticalNative
+ private static native void nSetTextAlign(long paintPtr, int align);
+ @CriticalNative
+ private static native void nSetTextLocalesByMinikinLangListId(long paintPtr,
+ int mMinikinLangListId);
+ @CriticalNative
+ private static native void nSetShadowLayer(long paintPtr,
+ float radius, float dx, float dy, int color);
+ @CriticalNative
+ private static native boolean nHasShadowLayer(long paintPtr);
+ @CriticalNative
+ private static native float nGetLetterSpacing(long paintPtr);
+ @CriticalNative
+ private static native void nSetLetterSpacing(long paintPtr, float letterSpacing);
+ @CriticalNative
+ private static native float nGetWordSpacing(long paintPtr);
+ @CriticalNative
+ private static native void nSetWordSpacing(long paintPtr, float wordSpacing);
+ @CriticalNative
+ private static native int nGetHyphenEdit(long paintPtr);
+ @CriticalNative
+ private static native void nSetHyphenEdit(long paintPtr, int hyphen);
+ @CriticalNative
+ private static native void nSetStrokeMiter(long paintPtr, float miter);
+ @CriticalNative
+ private static native float nGetStrokeMiter(long paintPtr);
+ @CriticalNative
+ private static native void nSetStrokeWidth(long paintPtr, float width);
+ @CriticalNative
+ private static native float nGetStrokeWidth(long paintPtr);
+ @CriticalNative
+ private static native void nSetAlpha(long paintPtr, int a);
+ @CriticalNative
+ private static native void nSetDither(long paintPtr, boolean dither);
+ @CriticalNative
+ private static native int nGetFlags(long paintPtr);
+ @CriticalNative
+ private static native void nSetFlags(long paintPtr, int flags);
+ @CriticalNative
+ private static native int nGetHinting(long paintPtr);
+ @CriticalNative
+ private static native void nSetHinting(long paintPtr, int mode);
+ @CriticalNative
+ private static native void nSetAntiAlias(long paintPtr, boolean aa);
+ @CriticalNative
+ private static native void nSetLinearText(long paintPtr, boolean linearText);
+ @CriticalNative
+ private static native void nSetSubpixelText(long paintPtr, boolean subpixelText);
+ @CriticalNative
+ private static native void nSetUnderlineText(long paintPtr, boolean underlineText);
+ @CriticalNative
+ private static native void nSetFakeBoldText(long paintPtr, boolean fakeBoldText);
+ @CriticalNative
+ private static native void nSetFilterBitmap(long paintPtr, boolean filter);
+ @CriticalNative
+ private static native int nGetColor(long paintPtr);
+ @CriticalNative
+ private static native void nSetColor(long paintPtr, @ColorInt int color);
+ @CriticalNative
+ private static native int nGetAlpha(long paintPtr);
+ @CriticalNative
+ private static native void nSetStrikeThruText(long paintPtr, boolean strikeThruText);
+ @CriticalNative
+ private static native boolean nIsElegantTextHeight(long paintPtr);
+ @CriticalNative
+ private static native void nSetElegantTextHeight(long paintPtr, boolean elegant);
+ @CriticalNative
+ private static native float nGetTextSize(long paintPtr);
+ @CriticalNative
+ private static native float nGetTextScaleX(long paintPtr);
+ @CriticalNative
+ private static native void nSetTextScaleX(long paintPtr, float scaleX);
+ @CriticalNative
+ private static native float nGetTextSkewX(long paintPtr);
+ @CriticalNative
+ private static native void nSetTextSkewX(long paintPtr, float skewX);
+ @CriticalNative
+ private static native float nAscent(long paintPtr);
+ @CriticalNative
+ private static native float nDescent(long paintPtr);
+ @CriticalNative
+ private static native float nGetUnderlinePosition(long paintPtr);
+ @CriticalNative
+ private static native float nGetUnderlineThickness(long paintPtr);
+ @CriticalNative
+ private static native float nGetStrikeThruPosition(long paintPtr);
+ @CriticalNative
+ private static native float nGetStrikeThruThickness(long paintPtr);
+ @CriticalNative
+ private static native void nSetTextSize(long paintPtr, float textSize);
+}
diff --git a/android/graphics/PaintFlagsDrawFilter.java b/android/graphics/PaintFlagsDrawFilter.java
new file mode 100644
index 0000000..2326611
--- /dev/null
+++ b/android/graphics/PaintFlagsDrawFilter.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2008 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 android.graphics;
+
+public class PaintFlagsDrawFilter extends DrawFilter {
+ /**
+ * Subclass of DrawFilter that affects every paint by first clearing
+ * the specified clearBits in the paint's flags, and then setting the
+ * specified setBits in the paint's flags.
+ *
+ * @param clearBits These bits will be cleared in the paint's flags
+ * @param setBits These bits will be set in the paint's flags
+ */
+ public PaintFlagsDrawFilter(int clearBits, int setBits) {
+ // our native constructor can return 0, if the specified bits
+ // are effectively a no-op
+ mNativeInt = nativeConstructor(clearBits, setBits);
+ }
+
+ private static native long nativeConstructor(int clearBits, int setBits);
+}
+
diff --git a/android/graphics/PaintFlagsDrawFilter_Delegate.java b/android/graphics/PaintFlagsDrawFilter_Delegate.java
new file mode 100644
index 0000000..fa20746
--- /dev/null
+++ b/android/graphics/PaintFlagsDrawFilter_Delegate.java
@@ -0,0 +1,64 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.PaintFlagsDrawFilter
+ *
+ * Through the layoutlib_create tool, the original native methods of PaintFlagsDrawFilter have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original PaintFlagsDrawFilter class.
+ *
+ * Because this extends {@link DrawFilter_Delegate}, there's no need to use a
+ * {@link DelegateManager}, as all the DrawFilter classes will be added to the manager owned by
+ * {@link DrawFilter_Delegate}.
+ *
+ * @see DrawFilter_Delegate
+ *
+ */
+public class PaintFlagsDrawFilter_Delegate extends DrawFilter_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public boolean isSupported() {
+ return false;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ return "Paint Flags Draw Filters are not supported.";
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeConstructor(int clearBits, int setBits) {
+ PaintFlagsDrawFilter_Delegate newDelegate = new PaintFlagsDrawFilter_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/Paint_Delegate.java b/android/graphics/Paint_Delegate.java
new file mode 100644
index 0000000..ef45203
--- /dev/null
+++ b/android/graphics/Paint_Delegate.java
@@ -0,0 +1,1355 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.FontFamily_Delegate.FontVariant;
+import android.graphics.Paint.FontMetrics;
+import android.graphics.Paint.FontMetricsInt;
+import android.text.TextUtils;
+
+import java.awt.BasicStroke;
+import java.awt.Font;
+import java.awt.Shape;
+import java.awt.Stroke;
+import java.awt.Toolkit;
+import java.awt.geom.AffineTransform;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import libcore.util.NativeAllocationRegistry_Delegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.Paint
+ *
+ * Through the layoutlib_create tool, the original native methods of Paint have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original Paint class.
+ *
+ * @see DelegateManager
+ *
+ */
+public class Paint_Delegate {
+ private static final float DEFAULT_TEXT_SIZE = 20.f;
+ private static final float DEFAULT_TEXT_SCALE_X = 1.f;
+ private static final float DEFAULT_TEXT_SKEW_X = 0.f;
+
+ /**
+ * Class associating a {@link Font} and its {@link java.awt.FontMetrics}.
+ */
+ /*package*/ static final class FontInfo {
+ final Font mFont;
+ final java.awt.FontMetrics mMetrics;
+
+ FontInfo(@NonNull Font font, @NonNull java.awt.FontMetrics fontMetrics) {
+ this.mFont = font;
+ this.mMetrics = fontMetrics;
+ }
+ }
+
+ // ---- delegate manager ----
+ private static final DelegateManager<Paint_Delegate> sManager =
+ new DelegateManager<>(Paint_Delegate.class);
+ private static long sFinalizer = -1;
+
+ // ---- delegate helper data ----
+
+ // This list can contain null elements.
+ @Nullable
+ private List<FontInfo> mFonts;
+
+ // ---- delegate data ----
+ private int mFlags;
+ private int mColor;
+ private int mStyle;
+ private int mCap;
+ private int mJoin;
+ private int mTextAlign;
+ private Typeface_Delegate mTypeface;
+ private float mStrokeWidth;
+ private float mStrokeMiter;
+ private float mTextSize;
+ private float mTextScaleX;
+ private float mTextSkewX;
+ private int mHintingMode = Paint.HINTING_ON;
+ private int mHyphenEdit;
+ private float mLetterSpacing; // not used in actual text rendering.
+ private float mWordSpacing; // not used in actual text rendering.
+ // Variant of the font. A paint's variant can only be compact or elegant.
+ private FontVariant mFontVariant = FontVariant.COMPACT;
+
+ private int mPorterDuffMode = Xfermode.DEFAULT;
+ private ColorFilter_Delegate mColorFilter;
+ private Shader_Delegate mShader;
+ private PathEffect_Delegate mPathEffect;
+ private MaskFilter_Delegate mMaskFilter;
+
+ @SuppressWarnings("FieldCanBeLocal") // Used to store the locale for future use
+ private Locale mLocale = Locale.getDefault();
+
+ // ---- Public Helper methods ----
+
+ @Nullable
+ public static Paint_Delegate getDelegate(long native_paint) {
+ return sManager.getDelegate(native_paint);
+ }
+
+ /**
+ * Returns the list of {@link Font} objects.
+ */
+ @NonNull
+ public List<FontInfo> getFonts() {
+ if (mTypeface == null) {
+ return Collections.emptyList();
+ }
+
+ if (mFonts != null) {
+ return mFonts;
+ }
+
+ // Apply an optional transformation for skew and scale
+ AffineTransform affineTransform = mTextScaleX != 1.0 || mTextSkewX != 0 ?
+ new AffineTransform(mTextScaleX, mTextSkewX, 0, 1, 0, 0) :
+ null;
+
+ List<FontInfo> infoList = StreamSupport.stream(mTypeface.getFonts(mFontVariant).spliterator
+ (), false)
+ .map(font -> getFontInfo(font, mTextSize, affineTransform))
+ .collect(Collectors.toList());
+ mFonts = Collections.unmodifiableList(infoList);
+
+ return mFonts;
+ }
+
+ public boolean isAntiAliased() {
+ return (mFlags & Paint.ANTI_ALIAS_FLAG) != 0;
+ }
+
+ public boolean isFilterBitmap() {
+ return (mFlags & Paint.FILTER_BITMAP_FLAG) != 0;
+ }
+
+ public int getStyle() {
+ return mStyle;
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+
+ public int getAlpha() {
+ return mColor >>> 24;
+ }
+
+ public void setAlpha(int alpha) {
+ mColor = (alpha << 24) | (mColor & 0x00FFFFFF);
+ }
+
+ public int getTextAlign() {
+ return mTextAlign;
+ }
+
+ public float getStrokeWidth() {
+ return mStrokeWidth;
+ }
+
+ /**
+ * returns the value of stroke miter needed by the java api.
+ */
+ public float getJavaStrokeMiter() {
+ return mStrokeMiter;
+ }
+
+ public int getJavaCap() {
+ switch (Paint.sCapArray[mCap]) {
+ case BUTT:
+ return BasicStroke.CAP_BUTT;
+ case ROUND:
+ return BasicStroke.CAP_ROUND;
+ default:
+ case SQUARE:
+ return BasicStroke.CAP_SQUARE;
+ }
+ }
+
+ public int getJavaJoin() {
+ switch (Paint.sJoinArray[mJoin]) {
+ default:
+ case MITER:
+ return BasicStroke.JOIN_MITER;
+ case ROUND:
+ return BasicStroke.JOIN_ROUND;
+ case BEVEL:
+ return BasicStroke.JOIN_BEVEL;
+ }
+ }
+
+ public Stroke getJavaStroke() {
+ if (mPathEffect != null) {
+ if (mPathEffect.isSupported()) {
+ Stroke stroke = mPathEffect.getStroke(this);
+ assert stroke != null;
+ //noinspection ConstantConditions
+ if (stroke != null) {
+ return stroke;
+ }
+ } else {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_PATHEFFECT,
+ mPathEffect.getSupportMessage(),
+ null, null /*data*/);
+ }
+ }
+
+ // if no custom stroke as been set, set the default one.
+ return new BasicStroke(
+ getStrokeWidth(),
+ getJavaCap(),
+ getJavaJoin(),
+ getJavaStrokeMiter());
+ }
+
+ /**
+ * Returns the {@link PorterDuff.Mode} as an int
+ */
+ public int getPorterDuffMode() {
+ return mPorterDuffMode;
+ }
+
+ /**
+ * Returns the {@link ColorFilter} delegate or null if none have been set
+ *
+ * @return the delegate or null.
+ */
+ public ColorFilter_Delegate getColorFilter() {
+ return mColorFilter;
+ }
+
+ public void setColorFilter(long colorFilterPtr) {
+ mColorFilter = ColorFilter_Delegate.getDelegate(colorFilterPtr);
+ }
+
+ public void setShader(long shaderPtr) {
+ mShader = Shader_Delegate.getDelegate(shaderPtr);
+ }
+
+ /**
+ * Returns the {@link Shader} delegate or null if none have been set
+ *
+ * @return the delegate or null.
+ */
+ public Shader_Delegate getShader() {
+ return mShader;
+ }
+
+ /**
+ * Returns the {@link MaskFilter} delegate or null if none have been set
+ *
+ * @return the delegate or null.
+ */
+ public MaskFilter_Delegate getMaskFilter() {
+ return mMaskFilter;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetFlags(long nativePaint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mFlags;
+ }
+
+
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetFlags(long nativePaint, int flags) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mFlags = flags;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetFilterBitmap(long nativePaint, boolean filter) {
+ setFlag(nativePaint, Paint.FILTER_BITMAP_FLAG, filter);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetHinting(long nativePaint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return Paint.HINTING_ON;
+ }
+
+ return delegate.mHintingMode;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetHinting(long nativePaint, int mode) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mHintingMode = mode;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetAntiAlias(long nativePaint, boolean aa) {
+ setFlag(nativePaint, Paint.ANTI_ALIAS_FLAG, aa);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetSubpixelText(long nativePaint,
+ boolean subpixelText) {
+ setFlag(nativePaint, Paint.SUBPIXEL_TEXT_FLAG, subpixelText);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetUnderlineText(long nativePaint,
+ boolean underlineText) {
+ setFlag(nativePaint, Paint.UNDERLINE_TEXT_FLAG, underlineText);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetStrikeThruText(long nativePaint,
+ boolean strikeThruText) {
+ setFlag(nativePaint, Paint.STRIKE_THRU_TEXT_FLAG, strikeThruText);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetFakeBoldText(long nativePaint,
+ boolean fakeBoldText) {
+ setFlag(nativePaint, Paint.FAKE_BOLD_TEXT_FLAG, fakeBoldText);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetDither(long nativePaint, boolean dither) {
+ setFlag(nativePaint, Paint.DITHER_FLAG, dither);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetLinearText(long nativePaint, boolean linearText) {
+ setFlag(nativePaint, Paint.LINEAR_TEXT_FLAG, linearText);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetColor(long nativePaint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mColor;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetColor(long nativePaint, int color) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mColor = color;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetAlpha(long nativePaint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.getAlpha();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetAlpha(long nativePaint, int a) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.setAlpha(a);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetStrokeWidth(long nativePaint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 1.f;
+ }
+
+ return delegate.mStrokeWidth;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetStrokeWidth(long nativePaint, float width) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mStrokeWidth = width;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetStrokeMiter(long nativePaint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 1.f;
+ }
+
+ return delegate.mStrokeMiter;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetStrokeMiter(long nativePaint, float miter) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mStrokeMiter = miter;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetShadowLayer(long paint, float radius, float dx, float dy,
+ int color) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Paint.setShadowLayer is not supported.", null, null /*data*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nHasShadowLayer(long paint) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Paint.hasShadowLayer is not supported.", null, null /*data*/);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nIsElegantTextHeight(long nativePaint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ return delegate != null && delegate.mFontVariant == FontVariant.ELEGANT;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetElegantTextHeight(long nativePaint,
+ boolean elegant) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mFontVariant = elegant ? FontVariant.ELEGANT : FontVariant.COMPACT;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetTextSize(long nativePaint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 1.f;
+ }
+
+ return delegate.mTextSize;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetTextSize(long nativePaint, float textSize) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ if (delegate.mTextSize != textSize) {
+ delegate.mTextSize = textSize;
+ delegate.invalidateFonts();
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetTextScaleX(long nativePaint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 1.f;
+ }
+
+ return delegate.mTextScaleX;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetTextScaleX(long nativePaint, float scaleX) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ if (delegate.mTextScaleX != scaleX) {
+ delegate.mTextScaleX = scaleX;
+ delegate.invalidateFonts();
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetTextSkewX(long nativePaint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 1.f;
+ }
+
+ return delegate.mTextSkewX;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetTextSkewX(long nativePaint, float skewX) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ if (delegate.mTextSkewX != skewX) {
+ delegate.mTextSkewX = skewX;
+ delegate.invalidateFonts();
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nAscent(long nativePaint) {
+ // get the delegate
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+
+ List<FontInfo> fonts = delegate.getFonts();
+ if (fonts.size() > 0) {
+ java.awt.FontMetrics javaMetrics = fonts.get(0).mMetrics;
+ // Android expects negative ascent so we invert the value from Java.
+ return - javaMetrics.getAscent();
+ }
+
+ return 0;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nDescent(long nativePaint) {
+ // get the delegate
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+
+ List<FontInfo> fonts = delegate.getFonts();
+ if (fonts.size() > 0) {
+ java.awt.FontMetrics javaMetrics = fonts.get(0).mMetrics;
+ return javaMetrics.getDescent();
+ }
+
+ return 0;
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetFontMetrics(long nativePaint,
+ FontMetrics metrics) {
+ // get the delegate
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.getFontMetrics(metrics);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetFontMetricsInt(long nativePaint, FontMetricsInt fmi) {
+ // get the delegate
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+
+ List<FontInfo> fonts = delegate.getFonts();
+ if (fonts.size() > 0) {
+ java.awt.FontMetrics javaMetrics = fonts.get(0).mMetrics;
+ if (fmi != null) {
+ // Android expects negative ascent so we invert the value from Java.
+ fmi.top = (int)(- javaMetrics.getMaxAscent() * 1.15);
+ fmi.ascent = - javaMetrics.getAscent();
+ fmi.descent = javaMetrics.getDescent();
+ fmi.bottom = (int)(javaMetrics.getMaxDescent() * 1.15);
+ fmi.leading = javaMetrics.getLeading();
+ }
+
+ return javaMetrics.getHeight();
+ }
+
+ return 0;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nBreakText(long nativePaint, char[] text,
+ int index, int count, float maxWidth, int bidiFlags, float[] measuredWidth) {
+
+ // get the delegate
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+
+ int inc = count > 0 ? 1 : -1;
+
+ int measureIndex = 0;
+ for (int i = index; i != index + count; i += inc, measureIndex++) {
+ int start, end;
+ if (i < index) {
+ start = i;
+ end = index;
+ } else {
+ start = index;
+ end = i;
+ }
+
+ // measure from start to end
+ RectF bounds = delegate.measureText(text, start, end - start + 1, null, 0, bidiFlags);
+ float res = bounds.right - bounds.left;
+
+ if (measuredWidth != null) {
+ measuredWidth[measureIndex] = res;
+ }
+
+ if (res > maxWidth) {
+ // we should not return this char index, but since it's 0-based
+ // and we need to return a count, we simply return measureIndex;
+ return measureIndex;
+ }
+
+ }
+
+ return measureIndex;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nBreakText(long nativePaint, String text, boolean measureForwards,
+ float maxWidth, int bidiFlags, float[] measuredWidth) {
+ return nBreakText(nativePaint, text.toCharArray(), 0, text.length(),
+ maxWidth, bidiFlags, measuredWidth);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nInit() {
+ Paint_Delegate newDelegate = new Paint_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nInitWithPaint(long paint) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(paint);
+ if (delegate == null) {
+ return 0;
+ }
+
+ Paint_Delegate newDelegate = new Paint_Delegate(delegate);
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nReset(long native_object) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.reset();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSet(long native_dst, long native_src) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate_dst = sManager.getDelegate(native_dst);
+ if (delegate_dst == null) {
+ return;
+ }
+
+ // get the delegate from the native int.
+ Paint_Delegate delegate_src = sManager.getDelegate(native_src);
+ if (delegate_src == null) {
+ return;
+ }
+
+ delegate_dst.set(delegate_src);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetStyle(long native_object) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mStyle;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetStyle(long native_object, int style) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mStyle = style;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetStrokeCap(long native_object) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mCap;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetStrokeCap(long native_object, int cap) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mCap = cap;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetStrokeJoin(long native_object) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mJoin;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetStrokeJoin(long native_object, int join) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mJoin = join;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nGetFillPath(long native_object, long src, long dst) {
+ Paint_Delegate paint = sManager.getDelegate(native_object);
+ if (paint == null) {
+ return false;
+ }
+
+ Path_Delegate srcPath = Path_Delegate.getDelegate(src);
+ if (srcPath == null) {
+ return true;
+ }
+
+ Path_Delegate dstPath = Path_Delegate.getDelegate(dst);
+ if (dstPath == null) {
+ return true;
+ }
+
+ Stroke stroke = paint.getJavaStroke();
+ Shape strokeShape = stroke.createStrokedShape(srcPath.getJavaShape());
+
+ dstPath.setJavaShape(strokeShape);
+
+ // FIXME figure out the return value?
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nSetShader(long native_object, long shader) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return shader;
+ }
+
+ delegate.mShader = Shader_Delegate.getDelegate(shader);
+
+ return shader;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nSetColorFilter(long native_object, long filter) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return filter;
+ }
+
+ delegate.mColorFilter = ColorFilter_Delegate.getDelegate(filter);
+
+ // Log warning if it's not supported.
+ if (delegate.mColorFilter != null && !delegate.mColorFilter.isSupported()) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_COLORFILTER,
+ delegate.mColorFilter.getSupportMessage(), null, null /*data*/);
+ }
+
+ return filter;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetXfermode(long native_object, int xfermode) {
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return;
+ }
+ delegate.mPorterDuffMode = xfermode;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nSetPathEffect(long native_object, long effect) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return effect;
+ }
+
+ delegate.mPathEffect = PathEffect_Delegate.getDelegate(effect);
+
+ return effect;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nSetMaskFilter(long native_object, long maskfilter) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return maskfilter;
+ }
+
+ delegate.mMaskFilter = MaskFilter_Delegate.getDelegate(maskfilter);
+
+ // since none of those are supported, display a fidelity warning right away
+ if (delegate.mMaskFilter != null && !delegate.mMaskFilter.isSupported()) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MASKFILTER,
+ delegate.mMaskFilter.getSupportMessage(), null, null /*data*/);
+ }
+
+ return maskfilter;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetTypeface(long native_object, long typeface) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return;
+ }
+
+ Typeface_Delegate typefaceDelegate = Typeface_Delegate.getDelegate(typeface);
+ if (delegate.mTypeface != typefaceDelegate) {
+ delegate.mTypeface = typefaceDelegate;
+ delegate.invalidateFonts();
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetTextAlign(long native_object) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mTextAlign;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetTextAlign(long native_object, int align) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.mTextAlign = align;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nSetTextLocales(long native_object, String locale) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return 0;
+ }
+
+ delegate.setTextLocale(locale);
+ return 0;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetTextLocalesByMinikinLangListId(long paintPtr,
+ int mMinikinLangListId) {
+ // FIXME
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetTextAdvances(long native_object, char[] text, int index,
+ int count, int contextIndex, int contextCount,
+ int bidiFlags, float[] advances, int advancesIndex) {
+
+ if (advances != null)
+ for (int i = advancesIndex; i< advancesIndex+count; i++)
+ advances[i]=0;
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(native_object);
+ if (delegate == null) {
+ return 0.f;
+ }
+
+ RectF bounds = delegate.measureText(text, index, count, advances, advancesIndex, bidiFlags);
+ return bounds.right - bounds.left;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetTextAdvances(long native_object, String text, int start, int end,
+ int contextStart, int contextEnd, int bidiFlags, float[] advances, int advancesIndex) {
+ // FIXME: support contextStart and contextEnd
+ int count = end - start;
+ char[] buffer = TemporaryBuffer.obtain(count);
+ TextUtils.getChars(text, start, end, buffer, 0);
+
+ return nGetTextAdvances(native_object, buffer, 0, count,
+ contextStart, contextEnd - contextStart, bidiFlags, advances, advancesIndex);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetTextRunCursor(Paint paint, long native_object, char[] text,
+ int contextStart, int contextLength, int flags, int offset, int cursorOpt) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Paint.getTextRunCursor is not supported.", null, null /*data*/);
+ return 0;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetTextRunCursor(Paint paint, long native_object, String text,
+ int contextStart, int contextEnd, int flags, int offset, int cursorOpt) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Paint.getTextRunCursor is not supported.", null, null /*data*/);
+ return 0;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nGetTextPath(long native_object, int bidiFlags, char[] text,
+ int index, int count, float x, float y, long path) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Paint.getTextPath is not supported.", null, null /*data*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nGetTextPath(long native_object, int bidiFlags, String text, int start,
+ int end, float x, float y, long path) {
+ // FIXME
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Paint.getTextPath is not supported.", null, null /*data*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nGetStringBounds(long nativePaint, String text, int start, int end,
+ int bidiFlags, Rect bounds) {
+ nGetCharArrayBounds(nativePaint, text.toCharArray(), start,
+ end - start, bidiFlags, bounds);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nGetCharArrayBounds(long nativePaint, char[] text, int index,
+ int count, int bidiFlags, Rect bounds) {
+
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ delegate.measureText(text, index, count, null, 0, bidiFlags).roundOut(bounds);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nGetNativeFinalizer() {
+ synchronized (Paint_Delegate.class) {
+ if (sFinalizer == -1) {
+ sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
+ sManager::removeJavaReferenceFor);
+ }
+ }
+ return sFinalizer;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetLetterSpacing(long nativePaint) {
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+ return delegate.mLetterSpacing;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetLetterSpacing(long nativePaint, float letterSpacing) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_TEXT_RENDERING,
+ "Paint.setLetterSpacing() not supported.", null, null);
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+ delegate.mLetterSpacing = letterSpacing;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetWordSpacing(long nativePaint) {
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+ return delegate.mWordSpacing;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetWordSpacing(long nativePaint, float wordSpacing) {
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+ delegate.mWordSpacing = wordSpacing;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetFontFeatureSettings(long nativePaint, String settings) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_TEXT_RENDERING,
+ "Paint.setFontFeatureSettings() not supported.", null, null);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetHyphenEdit(long nativePaint) {
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return 0;
+ }
+ return delegate.mHyphenEdit;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetHyphenEdit(long nativePaint, int hyphen) {
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+ delegate.mHyphenEdit = hyphen;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nHasGlyph(long nativePaint, int bidiFlags, String string) {
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return false;
+ }
+ if (string.length() == 0) {
+ return false;
+ }
+ if (string.length() > 1) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_TEXT_RENDERING,
+ "Paint.hasGlyph() is not supported for ligatures.", null, null);
+ return false;
+ }
+
+ char c = string.charAt(0);
+ for (Font font : delegate.mTypeface.getFonts(delegate.mFontVariant)) {
+ if (font.canDisplay(c)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetRunAdvance(long nativePaint, @NonNull char[] text, int start,
+ int end, int contextStart, int contextEnd,
+ boolean isRtl, int offset) {
+ int count = end - start;
+ float[] advances = new float[count];
+ int bidiFlags = isRtl ? Paint.BIDI_FORCE_RTL : Paint.BIDI_FORCE_LTR;
+ nGetTextAdvances(nativePaint, text, start, count, contextStart,
+ contextEnd - contextStart, bidiFlags, advances, 0);
+ int startOffset = offset - start; // offset from start.
+ float sum = 0;
+ for (int i = 0; i < startOffset; i++) {
+ sum += advances[i];
+ }
+ return sum;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetOffsetForAdvance(long nativePaint, char[] text, int start,
+ int end, int contextStart, int contextEnd, boolean isRtl, float advance) {
+ int count = end - start;
+ float[] advances = new float[count];
+ int bidiFlags = isRtl ? Paint.BIDI_FORCE_RTL : Paint.BIDI_FORCE_LTR;
+ nGetTextAdvances(nativePaint, text, start, count, contextStart,
+ contextEnd - contextStart, bidiFlags, advances, 0);
+ float sum = 0;
+ int i;
+ for (i = 0; i < count && sum < advance; i++) {
+ sum += advances[i];
+ }
+ float distanceToI = sum - advance;
+ float distanceToIMinus1 = advance - (sum - advances[i]);
+ return distanceToI > distanceToIMinus1 ? i : i - 1;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetUnderlinePosition(long paintPtr) {
+ return (1.0f / 9.0f) * nGetTextSize(paintPtr);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetUnderlineThickness(long paintPtr) {
+ return (1.0f / 18.0f) * nGetTextSize(paintPtr);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetStrikeThruPosition(long paintPtr) {
+ return (-79.0f / 252.0f) * nGetTextSize(paintPtr);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float nGetStrikeThruThickness(long paintPtr) {
+ return (1.0f / 18.0f) * nGetTextSize(paintPtr);
+ }
+
+ // ---- Private delegate/helper methods ----
+
+ /*package*/ Paint_Delegate() {
+ reset();
+ }
+
+ private Paint_Delegate(Paint_Delegate paint) {
+ set(paint);
+ }
+
+ private void set(Paint_Delegate paint) {
+ mFlags = paint.mFlags;
+ mColor = paint.mColor;
+ mStyle = paint.mStyle;
+ mCap = paint.mCap;
+ mJoin = paint.mJoin;
+ mTextAlign = paint.mTextAlign;
+
+ if (mTypeface != paint.mTypeface) {
+ mTypeface = paint.mTypeface;
+ invalidateFonts();
+ }
+
+ if (mTextSize != paint.mTextSize) {
+ mTextSize = paint.mTextSize;
+ invalidateFonts();
+ }
+
+ if (mTextScaleX != paint.mTextScaleX) {
+ mTextScaleX = paint.mTextScaleX;
+ invalidateFonts();
+ }
+
+ if (mTextSkewX != paint.mTextSkewX) {
+ mTextSkewX = paint.mTextSkewX;
+ invalidateFonts();
+ }
+
+ mStrokeWidth = paint.mStrokeWidth;
+ mStrokeMiter = paint.mStrokeMiter;
+ mPorterDuffMode = paint.mPorterDuffMode;
+ mColorFilter = paint.mColorFilter;
+ mShader = paint.mShader;
+ mPathEffect = paint.mPathEffect;
+ mMaskFilter = paint.mMaskFilter;
+ mHintingMode = paint.mHintingMode;
+ }
+
+ private void reset() {
+ Typeface_Delegate defaultTypeface =
+ Typeface_Delegate.getDelegate(Typeface.sDefaults[0].native_instance);
+
+ mFlags = Paint.HIDDEN_DEFAULT_PAINT_FLAGS;
+ mColor = 0xFF000000;
+ mStyle = Paint.Style.FILL.nativeInt;
+ mCap = Paint.Cap.BUTT.nativeInt;
+ mJoin = Paint.Join.MITER.nativeInt;
+ mTextAlign = 0;
+
+ if (mTypeface != defaultTypeface) {
+ mTypeface = defaultTypeface;
+ invalidateFonts();
+ }
+
+ mStrokeWidth = 1.f;
+ mStrokeMiter = 4.f;
+
+ if (mTextSize != DEFAULT_TEXT_SIZE) {
+ mTextSize = DEFAULT_TEXT_SIZE;
+ invalidateFonts();
+ }
+
+ if (mTextScaleX != DEFAULT_TEXT_SCALE_X) {
+ mTextScaleX = DEFAULT_TEXT_SCALE_X;
+ invalidateFonts();
+ }
+
+ if (mTextSkewX != DEFAULT_TEXT_SKEW_X) {
+ mTextSkewX = DEFAULT_TEXT_SKEW_X;
+ invalidateFonts();
+ }
+
+ mPorterDuffMode = Xfermode.DEFAULT;
+ mColorFilter = null;
+ mShader = null;
+ mPathEffect = null;
+ mMaskFilter = null;
+ mHintingMode = Paint.HINTING_ON;
+ }
+
+ private void invalidateFonts() {
+ mFonts = null;
+ }
+
+ @Nullable
+ private static FontInfo getFontInfo(@Nullable Font font, float textSize,
+ @Nullable AffineTransform transform) {
+ if (font == null) {
+ return null;
+ }
+
+ Font transformedFont = font.deriveFont(textSize);
+ if (transform != null) {
+ // TODO: support skew
+ transformedFont = transformedFont.deriveFont(transform);
+ }
+
+ // The metrics here don't have anti-aliasing set.
+ return new FontInfo(transformedFont,
+ Toolkit.getDefaultToolkit().getFontMetrics(transformedFont));
+ }
+
+ /*package*/ RectF measureText(char[] text, int index, int count, float[] advances,
+ int advancesIndex, int bidiFlags) {
+ return new BidiRenderer(null, this, text)
+ .renderText(index, index + count, bidiFlags, advances, advancesIndex, false);
+ }
+
+ /*package*/ RectF measureText(char[] text, int index, int count, float[] advances,
+ int advancesIndex, boolean isRtl) {
+ return new BidiRenderer(null, this, text)
+ .renderText(index, index + count, isRtl, advances, advancesIndex, false);
+ }
+
+ private float getFontMetrics(FontMetrics metrics) {
+ List<FontInfo> fonts = getFonts();
+ if (fonts.size() > 0) {
+ java.awt.FontMetrics javaMetrics = fonts.get(0).mMetrics;
+ if (metrics != null) {
+ // Android expects negative ascent so we invert the value from Java.
+ metrics.top = - javaMetrics.getMaxAscent();
+ metrics.ascent = - javaMetrics.getAscent();
+ metrics.descent = javaMetrics.getDescent();
+ metrics.bottom = javaMetrics.getMaxDescent();
+ metrics.leading = javaMetrics.getLeading();
+ }
+
+ return javaMetrics.getHeight();
+ }
+
+ return 0;
+ }
+
+ private void setTextLocale(String locale) {
+ mLocale = new Locale(locale);
+ }
+
+ private static void setFlag(long nativePaint, int flagMask, boolean flagValue) {
+ // get the delegate from the native int.
+ Paint_Delegate delegate = sManager.getDelegate(nativePaint);
+ if (delegate == null) {
+ return;
+ }
+
+ if (flagValue) {
+ delegate.mFlags |= flagMask;
+ } else {
+ delegate.mFlags &= ~flagMask;
+ }
+ }
+}
diff --git a/android/graphics/Path.java b/android/graphics/Path.java
new file mode 100644
index 0000000..098cdc6
--- /dev/null
+++ b/android/graphics/Path.java
@@ -0,0 +1,880 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+
+import dalvik.annotation.optimization.CriticalNative;
+import dalvik.annotation.optimization.FastNative;
+
+/**
+ * The Path class encapsulates compound (multiple contour) geometric paths
+ * consisting of straight line segments, quadratic curves, and cubic curves.
+ * It can be drawn with canvas.drawPath(path, paint), either filled or stroked
+ * (based on the paint's Style), or it can be used for clipping or to draw
+ * text on a path.
+ */
+public class Path {
+ /**
+ * @hide
+ */
+ public long mNativePath;
+
+ /**
+ * @hide
+ */
+ public boolean isSimplePath = true;
+ /**
+ * @hide
+ */
+ public Region rects;
+ private Direction mLastDirection = null;
+
+ /**
+ * Create an empty path
+ */
+ public Path() {
+ mNativePath = nInit();
+ }
+
+ /**
+ * Create a new path, copying the contents from the src path.
+ *
+ * @param src The path to copy from when initializing the new path
+ */
+ public Path(Path src) {
+ long valNative = 0;
+ if (src != null) {
+ valNative = src.mNativePath;
+ isSimplePath = src.isSimplePath;
+ if (src.rects != null) {
+ rects = new Region(src.rects);
+ }
+ }
+ mNativePath = nInit(valNative);
+ }
+
+ /**
+ * Clear any lines and curves from the path, making it empty.
+ * This does NOT change the fill-type setting.
+ */
+ public void reset() {
+ isSimplePath = true;
+ mLastDirection = null;
+ if (rects != null) rects.setEmpty();
+ // We promised not to change this, so preserve it around the native
+ // call, which does now reset fill type.
+ final FillType fillType = getFillType();
+ nReset(mNativePath);
+ setFillType(fillType);
+ }
+
+ /**
+ * Rewinds the path: clears any lines and curves from the path but
+ * keeps the internal data structure for faster reuse.
+ */
+ public void rewind() {
+ isSimplePath = true;
+ mLastDirection = null;
+ if (rects != null) rects.setEmpty();
+ nRewind(mNativePath);
+ }
+
+ /** Replace the contents of this with the contents of src.
+ */
+ public void set(@NonNull Path src) {
+ if (this == src) {
+ return;
+ }
+ isSimplePath = src.isSimplePath;
+ nSet(mNativePath, src.mNativePath);
+ if (!isSimplePath) {
+ return;
+ }
+
+ if (rects != null && src.rects != null) {
+ rects.set(src.rects);
+ } else if (rects != null && src.rects == null) {
+ rects.setEmpty();
+ } else if (src.rects != null) {
+ rects = new Region(src.rects);
+ }
+ }
+
+ /**
+ * The logical operations that can be performed when combining two paths.
+ *
+ * @see #op(Path, android.graphics.Path.Op)
+ * @see #op(Path, Path, android.graphics.Path.Op)
+ */
+ public enum Op {
+ /**
+ * Subtract the second path from the first path.
+ */
+ DIFFERENCE,
+ /**
+ * Intersect the two paths.
+ */
+ INTERSECT,
+ /**
+ * Union (inclusive-or) the two paths.
+ */
+ UNION,
+ /**
+ * Exclusive-or the two paths.
+ */
+ XOR,
+ /**
+ * Subtract the first path from the second path.
+ */
+ REVERSE_DIFFERENCE
+ }
+
+ /**
+ * Set this path to the result of applying the Op to this path and the specified path.
+ * The resulting path will be constructed from non-overlapping contours.
+ * The curve order is reduced where possible so that cubics may be turned
+ * into quadratics, and quadratics maybe turned into lines.
+ *
+ * @param path The second operand (for difference, the subtrahend)
+ *
+ * @return True if operation succeeded, false otherwise and this path remains unmodified.
+ *
+ * @see Op
+ * @see #op(Path, Path, android.graphics.Path.Op)
+ */
+ public boolean op(Path path, Op op) {
+ return op(this, path, op);
+ }
+
+ /**
+ * Set this path to the result of applying the Op to the two specified paths.
+ * The resulting path will be constructed from non-overlapping contours.
+ * The curve order is reduced where possible so that cubics may be turned
+ * into quadratics, and quadratics maybe turned into lines.
+ *
+ * @param path1 The first operand (for difference, the minuend)
+ * @param path2 The second operand (for difference, the subtrahend)
+ *
+ * @return True if operation succeeded, false otherwise and this path remains unmodified.
+ *
+ * @see Op
+ * @see #op(Path, android.graphics.Path.Op)
+ */
+ public boolean op(Path path1, Path path2, Op op) {
+ if (nOp(path1.mNativePath, path2.mNativePath, op.ordinal(), this.mNativePath)) {
+ isSimplePath = false;
+ rects = null;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the path's convexity, as defined by the content of the path.
+ * <p>
+ * A path is convex if it has a single contour, and only ever curves in a
+ * single direction.
+ * <p>
+ * This function will calculate the convexity of the path from its control
+ * points, and cache the result.
+ *
+ * @return True if the path is convex.
+ */
+ public boolean isConvex() {
+ return nIsConvex(mNativePath);
+ }
+
+ /**
+ * Enum for the ways a path may be filled.
+ */
+ public enum FillType {
+ // these must match the values in SkPath.h
+ /**
+ * Specifies that "inside" is computed by a non-zero sum of signed
+ * edge crossings.
+ */
+ WINDING (0),
+ /**
+ * Specifies that "inside" is computed by an odd number of edge
+ * crossings.
+ */
+ EVEN_ODD (1),
+ /**
+ * Same as {@link #WINDING}, but draws outside of the path, rather than inside.
+ */
+ INVERSE_WINDING (2),
+ /**
+ * Same as {@link #EVEN_ODD}, but draws outside of the path, rather than inside.
+ */
+ INVERSE_EVEN_ODD(3);
+
+ FillType(int ni) {
+ nativeInt = ni;
+ }
+
+ final int nativeInt;
+ }
+
+ // these must be in the same order as their native values
+ static final FillType[] sFillTypeArray = {
+ FillType.WINDING,
+ FillType.EVEN_ODD,
+ FillType.INVERSE_WINDING,
+ FillType.INVERSE_EVEN_ODD
+ };
+
+ /**
+ * Return the path's fill type. This defines how "inside" is
+ * computed. The default value is WINDING.
+ *
+ * @return the path's fill type
+ */
+ public FillType getFillType() {
+ return sFillTypeArray[nGetFillType(mNativePath)];
+ }
+
+ /**
+ * Set the path's fill type. This defines how "inside" is computed.
+ *
+ * @param ft The new fill type for this path
+ */
+ public void setFillType(FillType ft) {
+ nSetFillType(mNativePath, ft.nativeInt);
+ }
+
+ /**
+ * Returns true if the filltype is one of the INVERSE variants
+ *
+ * @return true if the filltype is one of the INVERSE variants
+ */
+ public boolean isInverseFillType() {
+ final int ft = nGetFillType(mNativePath);
+ return (ft & FillType.INVERSE_WINDING.nativeInt) != 0;
+ }
+
+ /**
+ * Toggles the INVERSE state of the filltype
+ */
+ public void toggleInverseFillType() {
+ int ft = nGetFillType(mNativePath);
+ ft ^= FillType.INVERSE_WINDING.nativeInt;
+ nSetFillType(mNativePath, ft);
+ }
+
+ /**
+ * Returns true if the path is empty (contains no lines or curves)
+ *
+ * @return true if the path is empty (contains no lines or curves)
+ */
+ public boolean isEmpty() {
+ return nIsEmpty(mNativePath);
+ }
+
+ /**
+ * Returns true if the path specifies a rectangle. If so, and if rect is
+ * not null, set rect to the bounds of the path. If the path does not
+ * specify a rectangle, return false and ignore rect.
+ *
+ * @param rect If not null, returns the bounds of the path if it specifies
+ * a rectangle
+ * @return true if the path specifies a rectangle
+ */
+ public boolean isRect(RectF rect) {
+ return nIsRect(mNativePath, rect);
+ }
+
+ /**
+ * Compute the bounds of the control points of the path, and write the
+ * answer into bounds. If the path contains 0 or 1 points, the bounds is
+ * set to (0,0,0,0)
+ *
+ * @param bounds Returns the computed bounds of the path's control points.
+ * @param exact This parameter is no longer used.
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ public void computeBounds(RectF bounds, boolean exact) {
+ nComputeBounds(mNativePath, bounds);
+ }
+
+ /**
+ * Hint to the path to prepare for adding more points. This can allow the
+ * path to more efficiently allocate its storage.
+ *
+ * @param extraPtCount The number of extra points that may be added to this
+ * path
+ */
+ public void incReserve(int extraPtCount) {
+ nIncReserve(mNativePath, extraPtCount);
+ }
+
+ /**
+ * Set the beginning of the next contour to the point (x,y).
+ *
+ * @param x The x-coordinate of the start of a new contour
+ * @param y The y-coordinate of the start of a new contour
+ */
+ public void moveTo(float x, float y) {
+ nMoveTo(mNativePath, x, y);
+ }
+
+ /**
+ * Set the beginning of the next contour relative to the last point on the
+ * previous contour. If there is no previous contour, this is treated the
+ * same as moveTo().
+ *
+ * @param dx The amount to add to the x-coordinate of the end of the
+ * previous contour, to specify the start of a new contour
+ * @param dy The amount to add to the y-coordinate of the end of the
+ * previous contour, to specify the start of a new contour
+ */
+ public void rMoveTo(float dx, float dy) {
+ nRMoveTo(mNativePath, dx, dy);
+ }
+
+ /**
+ * Add a line from the last point to the specified point (x,y).
+ * If no moveTo() call has been made for this contour, the first point is
+ * automatically set to (0,0).
+ *
+ * @param x The x-coordinate of the end of a line
+ * @param y The y-coordinate of the end of a line
+ */
+ public void lineTo(float x, float y) {
+ isSimplePath = false;
+ nLineTo(mNativePath, x, y);
+ }
+
+ /**
+ * Same as lineTo, but the coordinates are considered relative to the last
+ * point on this contour. If there is no previous point, then a moveTo(0,0)
+ * is inserted automatically.
+ *
+ * @param dx The amount to add to the x-coordinate of the previous point on
+ * this contour, to specify a line
+ * @param dy The amount to add to the y-coordinate of the previous point on
+ * this contour, to specify a line
+ */
+ public void rLineTo(float dx, float dy) {
+ isSimplePath = false;
+ nRLineTo(mNativePath, dx, dy);
+ }
+
+ /**
+ * Add a quadratic bezier from the last point, approaching control point
+ * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
+ * this contour, the first point is automatically set to (0,0).
+ *
+ * @param x1 The x-coordinate of the control point on a quadratic curve
+ * @param y1 The y-coordinate of the control point on a quadratic curve
+ * @param x2 The x-coordinate of the end point on a quadratic curve
+ * @param y2 The y-coordinate of the end point on a quadratic curve
+ */
+ public void quadTo(float x1, float y1, float x2, float y2) {
+ isSimplePath = false;
+ nQuadTo(mNativePath, x1, y1, x2, y2);
+ }
+
+ /**
+ * Same as quadTo, but the coordinates are considered relative to the last
+ * point on this contour. If there is no previous point, then a moveTo(0,0)
+ * is inserted automatically.
+ *
+ * @param dx1 The amount to add to the x-coordinate of the last point on
+ * this contour, for the control point of a quadratic curve
+ * @param dy1 The amount to add to the y-coordinate of the last point on
+ * this contour, for the control point of a quadratic curve
+ * @param dx2 The amount to add to the x-coordinate of the last point on
+ * this contour, for the end point of a quadratic curve
+ * @param dy2 The amount to add to the y-coordinate of the last point on
+ * this contour, for the end point of a quadratic curve
+ */
+ public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
+ isSimplePath = false;
+ nRQuadTo(mNativePath, dx1, dy1, dx2, dy2);
+ }
+
+ /**
+ * Add a cubic bezier from the last point, approaching control points
+ * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
+ * made for this contour, the first point is automatically set to (0,0).
+ *
+ * @param x1 The x-coordinate of the 1st control point on a cubic curve
+ * @param y1 The y-coordinate of the 1st control point on a cubic curve
+ * @param x2 The x-coordinate of the 2nd control point on a cubic curve
+ * @param y2 The y-coordinate of the 2nd control point on a cubic curve
+ * @param x3 The x-coordinate of the end point on a cubic curve
+ * @param y3 The y-coordinate of the end point on a cubic curve
+ */
+ public void cubicTo(float x1, float y1, float x2, float y2,
+ float x3, float y3) {
+ isSimplePath = false;
+ nCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
+ }
+
+ /**
+ * Same as cubicTo, but the coordinates are considered relative to the
+ * current point on this contour. If there is no previous point, then a
+ * moveTo(0,0) is inserted automatically.
+ */
+ public void rCubicTo(float x1, float y1, float x2, float y2,
+ float x3, float y3) {
+ isSimplePath = false;
+ nRCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
+ }
+
+ /**
+ * Append the specified arc to the path as a new contour. If the start of
+ * the path is different from the path's current last point, then an
+ * automatic lineTo() is added to connect the current contour to the
+ * start of the arc. However, if the path is empty, then we call moveTo()
+ * with the first point of the arc.
+ *
+ * @param oval The bounds of oval defining shape and size of the arc
+ * @param startAngle Starting angle (in degrees) where the arc begins
+ * @param sweepAngle Sweep angle (in degrees) measured clockwise, treated
+ * mod 360.
+ * @param forceMoveTo If true, always begin a new contour with the arc
+ */
+ public void arcTo(RectF oval, float startAngle, float sweepAngle,
+ boolean forceMoveTo) {
+ arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo);
+ }
+
+ /**
+ * Append the specified arc to the path as a new contour. If the start of
+ * the path is different from the path's current last point, then an
+ * automatic lineTo() is added to connect the current contour to the
+ * start of the arc. However, if the path is empty, then we call moveTo()
+ * with the first point of the arc.
+ *
+ * @param oval The bounds of oval defining shape and size of the arc
+ * @param startAngle Starting angle (in degrees) where the arc begins
+ * @param sweepAngle Sweep angle (in degrees) measured clockwise
+ */
+ public void arcTo(RectF oval, float startAngle, float sweepAngle) {
+ arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false);
+ }
+
+ /**
+ * Append the specified arc to the path as a new contour. If the start of
+ * the path is different from the path's current last point, then an
+ * automatic lineTo() is added to connect the current contour to the
+ * start of the arc. However, if the path is empty, then we call moveTo()
+ * with the first point of the arc.
+ *
+ * @param startAngle Starting angle (in degrees) where the arc begins
+ * @param sweepAngle Sweep angle (in degrees) measured clockwise, treated
+ * mod 360.
+ * @param forceMoveTo If true, always begin a new contour with the arc
+ */
+ public void arcTo(float left, float top, float right, float bottom, float startAngle,
+ float sweepAngle, boolean forceMoveTo) {
+ isSimplePath = false;
+ nArcTo(mNativePath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
+ }
+
+ /**
+ * Close the current contour. If the current point is not equal to the
+ * first point of the contour, a line segment is automatically added.
+ */
+ public void close() {
+ isSimplePath = false;
+ nClose(mNativePath);
+ }
+
+ /**
+ * Specifies how closed shapes (e.g. rects, ovals) are oriented when they
+ * are added to a path.
+ */
+ public enum Direction {
+ /** clockwise */
+ CW (0), // must match enum in SkPath.h
+ /** counter-clockwise */
+ CCW (1); // must match enum in SkPath.h
+
+ Direction(int ni) {
+ nativeInt = ni;
+ }
+ final int nativeInt;
+ }
+
+ private void detectSimplePath(float left, float top, float right, float bottom, Direction dir) {
+ if (mLastDirection == null) {
+ mLastDirection = dir;
+ }
+ if (mLastDirection != dir) {
+ isSimplePath = false;
+ } else {
+ if (rects == null) rects = new Region();
+ rects.op((int) left, (int) top, (int) right, (int) bottom, Region.Op.UNION);
+ }
+ }
+
+ /**
+ * Add a closed rectangle contour to the path
+ *
+ * @param rect The rectangle to add as a closed contour to the path
+ * @param dir The direction to wind the rectangle's contour
+ */
+ public void addRect(RectF rect, Direction dir) {
+ addRect(rect.left, rect.top, rect.right, rect.bottom, dir);
+ }
+
+ /**
+ * Add a closed rectangle contour to the path
+ *
+ * @param left The left side of a rectangle to add to the path
+ * @param top The top of a rectangle to add to the path
+ * @param right The right side of a rectangle to add to the path
+ * @param bottom The bottom of a rectangle to add to the path
+ * @param dir The direction to wind the rectangle's contour
+ */
+ public void addRect(float left, float top, float right, float bottom, Direction dir) {
+ detectSimplePath(left, top, right, bottom, dir);
+ nAddRect(mNativePath, left, top, right, bottom, dir.nativeInt);
+ }
+
+ /**
+ * Add a closed oval contour to the path
+ *
+ * @param oval The bounds of the oval to add as a closed contour to the path
+ * @param dir The direction to wind the oval's contour
+ */
+ public void addOval(RectF oval, Direction dir) {
+ addOval(oval.left, oval.top, oval.right, oval.bottom, dir);
+ }
+
+ /**
+ * Add a closed oval contour to the path
+ *
+ * @param dir The direction to wind the oval's contour
+ */
+ public void addOval(float left, float top, float right, float bottom, Direction dir) {
+ isSimplePath = false;
+ nAddOval(mNativePath, left, top, right, bottom, dir.nativeInt);
+ }
+
+ /**
+ * Add a closed circle contour to the path
+ *
+ * @param x The x-coordinate of the center of a circle to add to the path
+ * @param y The y-coordinate of the center of a circle to add to the path
+ * @param radius The radius of a circle to add to the path
+ * @param dir The direction to wind the circle's contour
+ */
+ public void addCircle(float x, float y, float radius, Direction dir) {
+ isSimplePath = false;
+ nAddCircle(mNativePath, x, y, radius, dir.nativeInt);
+ }
+
+ /**
+ * Add the specified arc to the path as a new contour.
+ *
+ * @param oval The bounds of oval defining the shape and size of the arc
+ * @param startAngle Starting angle (in degrees) where the arc begins
+ * @param sweepAngle Sweep angle (in degrees) measured clockwise
+ */
+ public void addArc(RectF oval, float startAngle, float sweepAngle) {
+ addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle);
+ }
+
+ /**
+ * Add the specified arc to the path as a new contour.
+ *
+ * @param startAngle Starting angle (in degrees) where the arc begins
+ * @param sweepAngle Sweep angle (in degrees) measured clockwise
+ */
+ public void addArc(float left, float top, float right, float bottom, float startAngle,
+ float sweepAngle) {
+ isSimplePath = false;
+ nAddArc(mNativePath, left, top, right, bottom, startAngle, sweepAngle);
+ }
+
+ /**
+ * Add a closed round-rectangle contour to the path
+ *
+ * @param rect The bounds of a round-rectangle to add to the path
+ * @param rx The x-radius of the rounded corners on the round-rectangle
+ * @param ry The y-radius of the rounded corners on the round-rectangle
+ * @param dir The direction to wind the round-rectangle's contour
+ */
+ public void addRoundRect(RectF rect, float rx, float ry, Direction dir) {
+ addRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, dir);
+ }
+
+ /**
+ * Add a closed round-rectangle contour to the path
+ *
+ * @param rx The x-radius of the rounded corners on the round-rectangle
+ * @param ry The y-radius of the rounded corners on the round-rectangle
+ * @param dir The direction to wind the round-rectangle's contour
+ */
+ public void addRoundRect(float left, float top, float right, float bottom, float rx, float ry,
+ Direction dir) {
+ isSimplePath = false;
+ nAddRoundRect(mNativePath, left, top, right, bottom, rx, ry, dir.nativeInt);
+ }
+
+ /**
+ * Add a closed round-rectangle contour to the path. Each corner receives
+ * two radius values [X, Y]. The corners are ordered top-left, top-right,
+ * bottom-right, bottom-left
+ *
+ * @param rect The bounds of a round-rectangle to add to the path
+ * @param radii Array of 8 values, 4 pairs of [X,Y] radii
+ * @param dir The direction to wind the round-rectangle's contour
+ */
+ public void addRoundRect(RectF rect, float[] radii, Direction dir) {
+ if (rect == null) {
+ throw new NullPointerException("need rect parameter");
+ }
+ addRoundRect(rect.left, rect.top, rect.right, rect.bottom, radii, dir);
+ }
+
+ /**
+ * Add a closed round-rectangle contour to the path. Each corner receives
+ * two radius values [X, Y]. The corners are ordered top-left, top-right,
+ * bottom-right, bottom-left
+ *
+ * @param radii Array of 8 values, 4 pairs of [X,Y] radii
+ * @param dir The direction to wind the round-rectangle's contour
+ */
+ public void addRoundRect(float left, float top, float right, float bottom, float[] radii,
+ Direction dir) {
+ if (radii.length < 8) {
+ throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values");
+ }
+ isSimplePath = false;
+ nAddRoundRect(mNativePath, left, top, right, bottom, radii, dir.nativeInt);
+ }
+
+ /**
+ * Add a copy of src to the path, offset by (dx,dy)
+ *
+ * @param src The path to add as a new contour
+ * @param dx The amount to translate the path in X as it is added
+ */
+ public void addPath(Path src, float dx, float dy) {
+ isSimplePath = false;
+ nAddPath(mNativePath, src.mNativePath, dx, dy);
+ }
+
+ /**
+ * Add a copy of src to the path
+ *
+ * @param src The path that is appended to the current path
+ */
+ public void addPath(Path src) {
+ isSimplePath = false;
+ nAddPath(mNativePath, src.mNativePath);
+ }
+
+ /**
+ * Add a copy of src to the path, transformed by matrix
+ *
+ * @param src The path to add as a new contour
+ */
+ public void addPath(Path src, Matrix matrix) {
+ if (!src.isSimplePath) isSimplePath = false;
+ nAddPath(mNativePath, src.mNativePath, matrix.native_instance);
+ }
+
+ /**
+ * Offset the path by (dx,dy)
+ *
+ * @param dx The amount in the X direction to offset the entire path
+ * @param dy The amount in the Y direction to offset the entire path
+ * @param dst The translated path is written here. If this is null, then
+ * the original path is modified.
+ */
+ public void offset(float dx, float dy, @Nullable Path dst) {
+ if (dst != null) {
+ dst.set(this);
+ } else {
+ dst = this;
+ }
+ dst.offset(dx, dy);
+ }
+
+ /**
+ * Offset the path by (dx,dy)
+ *
+ * @param dx The amount in the X direction to offset the entire path
+ * @param dy The amount in the Y direction to offset the entire path
+ */
+ public void offset(float dx, float dy) {
+ if (isSimplePath && rects == null) {
+ // nothing to offset
+ return;
+ }
+ if (isSimplePath && dx == Math.rint(dx) && dy == Math.rint(dy)) {
+ rects.translate((int) dx, (int) dy);
+ } else {
+ isSimplePath = false;
+ }
+ nOffset(mNativePath, dx, dy);
+ }
+
+ /**
+ * Sets the last point of the path.
+ *
+ * @param dx The new X coordinate for the last point
+ * @param dy The new Y coordinate for the last point
+ */
+ public void setLastPoint(float dx, float dy) {
+ isSimplePath = false;
+ nSetLastPoint(mNativePath, dx, dy);
+ }
+
+ /**
+ * Transform the points in this path by matrix, and write the answer
+ * into dst. If dst is null, then the the original path is modified.
+ *
+ * @param matrix The matrix to apply to the path
+ * @param dst The transformed path is written here. If dst is null,
+ * then the the original path is modified
+ */
+ public void transform(Matrix matrix, Path dst) {
+ long dstNative = 0;
+ if (dst != null) {
+ dst.isSimplePath = false;
+ dstNative = dst.mNativePath;
+ }
+ nTransform(mNativePath, matrix.native_instance, dstNative);
+ }
+
+ /**
+ * Transform the points in this path by matrix.
+ *
+ * @param matrix The matrix to apply to the path
+ */
+ public void transform(Matrix matrix) {
+ isSimplePath = false;
+ nTransform(mNativePath, matrix.native_instance);
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ nFinalize(mNativePath);
+ mNativePath = 0; // Other finalizers can still call us.
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /** @hide */
+ public final long readOnlyNI() {
+ return mNativePath;
+ }
+
+ final long mutateNI() {
+ isSimplePath = false;
+ return mNativePath;
+ }
+
+ /**
+ * Approximate the <code>Path</code> with a series of line segments.
+ * This returns float[] with the array containing point components.
+ * There are three components for each point, in order:
+ * <ul>
+ * <li>Fraction along the length of the path that the point resides</li>
+ * <li>The x coordinate of the point</li>
+ * <li>The y coordinate of the point</li>
+ * </ul>
+ * <p>Two points may share the same fraction along its length when there is
+ * a move action within the Path.</p>
+ *
+ * @param acceptableError The acceptable error for a line on the
+ * Path. Typically this would be 0.5 so that
+ * the error is less than half a pixel.
+ * @return An array of components for points approximating the Path.
+ */
+ @NonNull
+ @Size(min = 6, multiple = 3)
+ public float[] approximate(@FloatRange(from = 0) float acceptableError) {
+ if (acceptableError < 0) {
+ throw new IllegalArgumentException("AcceptableError must be greater than or equal to 0");
+ }
+ return nApproximate(mNativePath, acceptableError);
+ }
+
+ // ------------------ Regular JNI ------------------------
+
+ private static native long nInit();
+ private static native long nInit(long nPath);
+ private static native void nFinalize(long nPath);
+ private static native void nSet(long native_dst, long nSrc);
+ private static native void nComputeBounds(long nPath, RectF bounds);
+ private static native void nIncReserve(long nPath, int extraPtCount);
+ private static native void nMoveTo(long nPath, float x, float y);
+ private static native void nRMoveTo(long nPath, float dx, float dy);
+ private static native void nLineTo(long nPath, float x, float y);
+ private static native void nRLineTo(long nPath, float dx, float dy);
+ private static native void nQuadTo(long nPath, float x1, float y1, float x2, float y2);
+ private static native void nRQuadTo(long nPath, float dx1, float dy1, float dx2, float dy2);
+ private static native void nCubicTo(long nPath, float x1, float y1, float x2, float y2,
+ float x3, float y3);
+ private static native void nRCubicTo(long nPath, float x1, float y1, float x2, float y2,
+ float x3, float y3);
+ private static native void nArcTo(long nPath, float left, float top, float right, float bottom,
+ float startAngle, float sweepAngle, boolean forceMoveTo);
+ private static native void nClose(long nPath);
+ private static native void nAddRect(long nPath, float left, float top,
+ float right, float bottom, int dir);
+ private static native void nAddOval(long nPath, float left, float top,
+ float right, float bottom, int dir);
+ private static native void nAddCircle(long nPath, float x, float y, float radius, int dir);
+ private static native void nAddArc(long nPath, float left, float top, float right, float bottom,
+ float startAngle, float sweepAngle);
+ private static native void nAddRoundRect(long nPath, float left, float top,
+ float right, float bottom, float rx, float ry, int dir);
+ private static native void nAddRoundRect(long nPath, float left, float top,
+ float right, float bottom, float[] radii, int dir);
+ private static native void nAddPath(long nPath, long src, float dx, float dy);
+ private static native void nAddPath(long nPath, long src);
+ private static native void nAddPath(long nPath, long src, long matrix);
+ private static native void nOffset(long nPath, float dx, float dy);
+ private static native void nSetLastPoint(long nPath, float dx, float dy);
+ private static native void nTransform(long nPath, long matrix, long dst_path);
+ private static native void nTransform(long nPath, long matrix);
+ private static native boolean nOp(long path1, long path2, int op, long result);
+ private static native float[] nApproximate(long nPath, float error);
+
+ // ------------------ Fast JNI ------------------------
+
+ @FastNative
+ private static native boolean nIsRect(long nPath, RectF rect);
+
+ // ------------------ Critical JNI ------------------------
+
+ @CriticalNative
+ private static native void nReset(long nPath);
+ @CriticalNative
+ private static native void nRewind(long nPath);
+ @CriticalNative
+ private static native boolean nIsEmpty(long nPath);
+ @CriticalNative
+ private static native boolean nIsConvex(long nPath);
+ @CriticalNative
+ private static native int nGetFillType(long nPath);
+ @CriticalNative
+ private static native void nSetFillType(long nPath, int ft);
+}
diff --git a/android/graphics/PathDashPathEffect.java b/android/graphics/PathDashPathEffect.java
new file mode 100644
index 0000000..2b6a6ed
--- /dev/null
+++ b/android/graphics/PathDashPathEffect.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+public class PathDashPathEffect extends PathEffect {
+
+ public enum Style {
+ TRANSLATE(0), //!< translate the shape to each position
+ ROTATE(1), //!< rotate the shape about its center
+ MORPH(2); //!< transform each point, and turn lines into curves
+
+ Style(int value) {
+ native_style = value;
+ }
+ int native_style;
+ }
+
+ /**
+ * Dash the drawn path by stamping it with the specified shape. This only
+ * applies to drawings when the paint's style is STROKE or STROKE_AND_FILL.
+ * If the paint's style is FILL, then this effect is ignored. The paint's
+ * strokeWidth does not affect the results.
+ * @param shape The path to stamp along
+ * @param advance spacing between each stamp of shape
+ * @param phase amount to offset before the first shape is stamped
+ * @param style how to transform the shape at each position as it is stamped
+ */
+ public PathDashPathEffect(Path shape, float advance, float phase,
+ Style style) {
+ native_instance = nativeCreate(shape.readOnlyNI(), advance, phase,
+ style.native_style);
+ }
+
+ private static native long nativeCreate(long native_path, float advance,
+ float phase, int native_style);
+}
+
diff --git a/android/graphics/PathDashPathEffect_Delegate.java b/android/graphics/PathDashPathEffect_Delegate.java
new file mode 100644
index 0000000..fd9ba62
--- /dev/null
+++ b/android/graphics/PathDashPathEffect_Delegate.java
@@ -0,0 +1,72 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.awt.Stroke;
+
+/**
+ * Delegate implementing the native methods of android.graphics.PathDashPathEffect
+ *
+ * Through the layoutlib_create tool, the original native methods of PathDashPathEffect have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original PathDashPathEffect class.
+ *
+ * Because this extends {@link PathEffect_Delegate}, there's no need to use a {@link DelegateManager},
+ * as all the Shader classes will be added to the manager owned by {@link PathEffect_Delegate}.
+ *
+ * @see PathEffect_Delegate
+ *
+ */
+public class PathDashPathEffect_Delegate extends PathEffect_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public Stroke getStroke(Paint_Delegate paint) {
+ // FIXME
+ return null;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return false;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ return "Path Dash Path Effects are not supported in Layout Preview mode.";
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate(long native_path, float advance, float phase,
+ int native_style) {
+ PathDashPathEffect_Delegate newDelegate = new PathDashPathEffect_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/PathEffect.java b/android/graphics/PathEffect.java
new file mode 100644
index 0000000..3292501
--- /dev/null
+++ b/android/graphics/PathEffect.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * PathEffect is the base class for objects in the Paint that affect
+ * the geometry of a drawing primitive before it is transformed by the
+ * canvas' matrix and drawn.
+ */
+public class PathEffect {
+
+ protected void finalize() throws Throwable {
+ nativeDestructor(native_instance);
+ native_instance = 0; // Other finalizers can still call us.
+ }
+
+ private static native void nativeDestructor(long native_patheffect);
+ long native_instance;
+}
diff --git a/android/graphics/PathEffect_Delegate.java b/android/graphics/PathEffect_Delegate.java
new file mode 100644
index 0000000..000481e
--- /dev/null
+++ b/android/graphics/PathEffect_Delegate.java
@@ -0,0 +1,69 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.awt.Stroke;
+
+/**
+ * Delegate implementing the native methods of android.graphics.PathEffect
+ *
+ * Through the layoutlib_create tool, the original native methods of PathEffect have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original PathEffect class.
+ *
+ * This also serve as a base class for all PathEffect delegate classes.
+ *
+ * @see DelegateManager
+ *
+ */
+public abstract class PathEffect_Delegate {
+
+ // ---- delegate manager ----
+ protected static final DelegateManager<PathEffect_Delegate> sManager =
+ new DelegateManager<PathEffect_Delegate>(PathEffect_Delegate.class);
+
+ // ---- delegate helper data ----
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ public static PathEffect_Delegate getDelegate(long nativeShader) {
+ return sManager.getDelegate(nativeShader);
+ }
+
+ public abstract Stroke getStroke(Paint_Delegate paint);
+ public abstract boolean isSupported();
+ public abstract String getSupportMessage();
+
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeDestructor(long native_patheffect) {
+ sManager.removeJavaReferenceFor(native_patheffect);
+ }
+
+ // ---- Private delegate/helper methods ----
+
+}
diff --git a/android/graphics/PathMeasure.java b/android/graphics/PathMeasure.java
new file mode 100644
index 0000000..bf79656
--- /dev/null
+++ b/android/graphics/PathMeasure.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+public class PathMeasure {
+ private Path mPath;
+
+ /**
+ * Create an empty PathMeasure object. To uses this to measure the length
+ * of a path, and/or to find the position and tangent along it, call
+ * setPath.
+ *
+ * Note that once a path is associated with the measure object, it is
+ * undefined if the path is subsequently modified and the the measure object
+ * is used. If the path is modified, you must call setPath with the path.
+ */
+ public PathMeasure() {
+ mPath = null;
+ native_instance = native_create(0, false);
+ }
+
+ /**
+ * Create a PathMeasure object associated with the specified path object
+ * (already created and specified). The measure object can now return the
+ * path's length, and the position and tangent of any position along the
+ * path.
+ *
+ * Note that once a path is associated with the measure object, it is
+ * undefined if the path is subsequently modified and the the measure object
+ * is used. If the path is modified, you must call setPath with the path.
+ *
+ * @param path The path that will be measured by this object
+ * @param forceClosed If true, then the path will be considered as "closed"
+ * even if its contour was not explicitly closed.
+ */
+ public PathMeasure(Path path, boolean forceClosed) {
+ // The native implementation does not copy the path, prevent it from being GC'd
+ mPath = path;
+ native_instance = native_create(path != null ? path.readOnlyNI() : 0,
+ forceClosed);
+ }
+
+ /**
+ * Assign a new path, or null to have none.
+ */
+ public void setPath(Path path, boolean forceClosed) {
+ mPath = path;
+ native_setPath(native_instance,
+ path != null ? path.readOnlyNI() : 0,
+ forceClosed);
+ }
+
+ /**
+ * Return the total length of the current contour, or 0 if no path is
+ * associated with this measure object.
+ */
+ public float getLength() {
+ return native_getLength(native_instance);
+ }
+
+ /**
+ * Pins distance to 0 <= distance <= getLength(), and then computes the
+ * corresponding position and tangent. Returns false if there is no path,
+ * or a zero-length path was specified, in which case position and tangent
+ * are unchanged.
+ *
+ * @param distance The distance along the current contour to sample
+ * @param pos If not null, returns the sampled position (x==[0], y==[1])
+ * @param tan If not null, returns the sampled tangent (x==[0], y==[1])
+ * @return false if there was no path associated with this measure object
+ */
+ public boolean getPosTan(float distance, float pos[], float tan[]) {
+ if (pos != null && pos.length < 2 ||
+ tan != null && tan.length < 2) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ return native_getPosTan(native_instance, distance, pos, tan);
+ }
+
+ public static final int POSITION_MATRIX_FLAG = 0x01; // must match flags in SkPathMeasure.h
+ public static final int TANGENT_MATRIX_FLAG = 0x02; // must match flags in SkPathMeasure.h
+
+ /**
+ * Pins distance to 0 <= distance <= getLength(), and then computes the
+ * corresponding matrix. Returns false if there is no path, or a zero-length
+ * path was specified, in which case matrix is unchanged.
+ *
+ * @param distance The distance along the associated path
+ * @param matrix Allocated by the caller, this is set to the transformation
+ * associated with the position and tangent at the specified distance
+ * @param flags Specified what aspects should be returned in the matrix.
+ */
+ public boolean getMatrix(float distance, Matrix matrix, int flags) {
+ return native_getMatrix(native_instance, distance, matrix.native_instance, flags);
+ }
+
+ /**
+ * Given a start and stop distance, return in dst the intervening
+ * segment(s). If the segment is zero-length, return false, else return
+ * true. startD and stopD are pinned to legal values (0..getLength()).
+ * If startD >= stopD then return false (and leave dst untouched).
+ * Begin the segment with a moveTo if startWithMoveTo is true.
+ *
+ * <p>On {@link android.os.Build.VERSION_CODES#KITKAT} and earlier
+ * releases, the resulting path may not display on a hardware-accelerated
+ * Canvas. A simple workaround is to add a single operation to this path,
+ * such as <code>dst.rLineTo(0, 0)</code>.</p>
+ */
+ public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) {
+ // Skia used to enforce this as part of it's API, but has since relaxed that restriction
+ // so to maintain consistency in our API we enforce the preconditions here.
+ float length = getLength();
+ if (startD < 0) {
+ startD = 0;
+ }
+ if (stopD > length) {
+ stopD = length;
+ }
+ if (startD >= stopD) {
+ return false;
+ }
+
+ return native_getSegment(native_instance, startD, stopD, dst.mutateNI(), startWithMoveTo);
+ }
+
+ /**
+ * Return true if the current contour is closed()
+ */
+ public boolean isClosed() {
+ return native_isClosed(native_instance);
+ }
+
+ /**
+ * Move to the next contour in the path. Return true if one exists, or
+ * false if we're done with the path.
+ */
+ public boolean nextContour() {
+ return native_nextContour(native_instance);
+ }
+
+ protected void finalize() throws Throwable {
+ native_destroy(native_instance);
+ native_instance = 0; // Other finalizers can still call us.
+ }
+
+ private static native long native_create(long native_path, boolean forceClosed);
+ private static native void native_setPath(long native_instance, long native_path, boolean forceClosed);
+ private static native float native_getLength(long native_instance);
+ private static native boolean native_getPosTan(long native_instance, float distance, float pos[], float tan[]);
+ private static native boolean native_getMatrix(long native_instance, float distance, long native_matrix, int flags);
+ private static native boolean native_getSegment(long native_instance, float startD, float stopD, long native_path, boolean startWithMoveTo);
+ private static native boolean native_isClosed(long native_instance);
+ private static native boolean native_nextContour(long native_instance);
+ private static native void native_destroy(long native_instance);
+
+ /* package */private long native_instance;
+}
+
diff --git a/android/graphics/PathMeasure_Delegate.java b/android/graphics/PathMeasure_Delegate.java
new file mode 100644
index 0000000..7f707c9
--- /dev/null
+++ b/android/graphics/PathMeasure_Delegate.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2015 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.layoutlib.bridge.util.CachedPathIteratorFactory;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import com.android.layoutlib.bridge.util.CachedPathIteratorFactory.CachedPathIterator;
+
+import java.awt.geom.PathIterator;
+
+/**
+ * Delegate implementing the native methods of {@link android.graphics.PathMeasure}
+ * <p/>
+ * Through the layoutlib_create tool, the original native methods of PathMeasure have been
+ * replaced by
+ * calls to methods of the same name in this delegate class.
+ * <p/>
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between it
+ * and the original PathMeasure class.
+ *
+ * @see DelegateManager
+ */
+public final class PathMeasure_Delegate {
+
+ // ---- delegate manager ----
+ private static final DelegateManager<PathMeasure_Delegate> sManager =
+ new DelegateManager<PathMeasure_Delegate>(PathMeasure_Delegate.class);
+
+ // ---- delegate data ----
+ private CachedPathIteratorFactory mOriginalPathIterator;
+
+ private long mNativePath;
+
+
+ private PathMeasure_Delegate(long native_path, boolean forceClosed) {
+ mNativePath = native_path;
+ if (native_path != 0) {
+ if (forceClosed) {
+ // Copy the path and call close
+ native_path = Path_Delegate.nInit(native_path);
+ Path_Delegate.nClose(native_path);
+ }
+
+ Path_Delegate pathDelegate = Path_Delegate.getDelegate(native_path);
+ mOriginalPathIterator = new CachedPathIteratorFactory(pathDelegate.getJavaShape()
+ .getPathIterator(null));
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long native_create(long native_path, boolean forceClosed) {
+ return sManager.addNewDelegate(new PathMeasure_Delegate(native_path, forceClosed));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void native_destroy(long native_instance) {
+ sManager.removeJavaReferenceFor(native_instance);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean native_getPosTan(long native_instance, float distance, float pos[],
+ float tan[]) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "PathMeasure.getPostTan is not supported.", null, null);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean native_getMatrix(long native_instance, float distance, long
+ native_matrix, int flags) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "PathMeasure.getMatrix is not supported.", null, null);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean native_nextContour(long native_instance) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "PathMeasure.nextContour is not supported.", null, null);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void native_setPath(long native_instance, long native_path, boolean
+ forceClosed) {
+ PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance);
+ assert pathMeasure != null;
+
+ if (native_path != 0) {
+ if (forceClosed) {
+ // Copy the path and call close
+ native_path = Path_Delegate.nInit(native_path);
+ Path_Delegate.nClose(native_path);
+ }
+
+ Path_Delegate pathDelegate = Path_Delegate.getDelegate(native_path);
+ pathMeasure.mOriginalPathIterator = new CachedPathIteratorFactory(pathDelegate.getJavaShape()
+ .getPathIterator(null));
+ }
+
+ pathMeasure.mNativePath = native_path;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float native_getLength(long native_instance) {
+ PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance);
+ assert pathMeasure != null;
+
+ if (pathMeasure.mOriginalPathIterator == null) {
+ return 0;
+ }
+
+ return pathMeasure.mOriginalPathIterator.iterator().getTotalLength();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean native_isClosed(long native_instance) {
+ PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance);
+ assert pathMeasure != null;
+
+ Path_Delegate path = Path_Delegate.getDelegate(pathMeasure.mNativePath);
+ if (path == null) {
+ return false;
+ }
+
+ int type = 0;
+ float segment[] = new float[6];
+ for (PathIterator pi = path.getJavaShape().getPathIterator(null); !pi.isDone(); pi.next()) {
+ type = pi.currentSegment(segment);
+ }
+
+ // A path is a closed path if the last element is SEG_CLOSE
+ return type == PathIterator.SEG_CLOSE;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean native_getSegment(long native_instance, float startD, float stopD,
+ long native_dst_path, boolean startWithMoveTo) {
+ if (startD < 0) {
+ startD = 0;
+ }
+
+ if (startD >= stopD) {
+ return false;
+ }
+
+ PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance);
+ assert pathMeasure != null;
+
+ CachedPathIterator iterator = pathMeasure.mOriginalPathIterator.iterator();
+ float accLength = startD;
+ boolean isZeroLength = true; // Whether the output has zero length or not
+ float[] points = new float[6];
+
+ iterator.jumpToSegment(accLength);
+ while (!iterator.isDone() && (stopD - accLength > 0.1f)) {
+ int type = iterator.currentSegment(points, stopD - accLength);
+
+ if (accLength - iterator.getCurrentSegmentLength() <= stopD) {
+ if (startWithMoveTo) {
+ startWithMoveTo = false;
+
+ // If this segment is a MOVETO, then we just use that one. If not, then we issue
+ // a first moveto
+ if (type != PathIterator.SEG_MOVETO) {
+ float[] lastPoint = new float[2];
+ iterator.getCurrentSegmentEnd(lastPoint);
+ Path_Delegate.nMoveTo(native_dst_path, lastPoint[0], lastPoint[1]);
+ }
+ }
+
+ isZeroLength = isZeroLength && iterator.getCurrentSegmentLength() > 0;
+ switch (type) {
+ case PathIterator.SEG_MOVETO:
+ Path_Delegate.nMoveTo(native_dst_path, points[0], points[1]);
+ break;
+ case PathIterator.SEG_LINETO:
+ Path_Delegate.nLineTo(native_dst_path, points[0], points[1]);
+ break;
+ case PathIterator.SEG_CLOSE:
+ Path_Delegate.nClose(native_dst_path);
+ break;
+ case PathIterator.SEG_CUBICTO:
+ Path_Delegate.nCubicTo(native_dst_path, points[0], points[1],
+ points[2], points[3],
+ points[4], points[5]);
+ break;
+ case PathIterator.SEG_QUADTO:
+ Path_Delegate.nQuadTo(native_dst_path, points[0], points[1],
+ points[2],
+ points[3]);
+ break;
+ default:
+ assert false;
+ }
+ }
+
+ accLength += iterator.getCurrentSegmentLength();
+ iterator.next();
+ }
+
+ return !isZeroLength;
+ }
+}
diff --git a/android/graphics/Path_Delegate.java b/android/graphics/Path_Delegate.java
new file mode 100644
index 0000000..0979017
--- /dev/null
+++ b/android/graphics/Path_Delegate.java
@@ -0,0 +1,899 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.NonNull;
+import android.graphics.Path.Direction;
+import android.graphics.Path.FillType;
+
+import java.awt.Shape;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Arc2D;
+import java.awt.geom.Area;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.geom.RoundRectangle2D;
+import java.util.ArrayList;
+
+/**
+ * Delegate implementing the native methods of android.graphics.Path
+ *
+ * Through the layoutlib_create tool, the original native methods of Path have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original Path class.
+ *
+ * @see DelegateManager
+ *
+ */
+public final class Path_Delegate {
+
+ // ---- delegate manager ----
+ private static final DelegateManager<Path_Delegate> sManager =
+ new DelegateManager<Path_Delegate>(Path_Delegate.class);
+
+ private static final float EPSILON = 1e-4f;
+
+ // ---- delegate data ----
+ private FillType mFillType = FillType.WINDING;
+ private Path2D mPath = new Path2D.Double();
+
+ private float mLastX = 0;
+ private float mLastY = 0;
+
+ // true if the path contains does not contain a curve or line.
+ private boolean mCachedIsEmpty = true;
+
+ // ---- Public Helper methods ----
+
+ public static Path_Delegate getDelegate(long nPath) {
+ return sManager.getDelegate(nPath);
+ }
+
+ public Path2D getJavaShape() {
+ return mPath;
+ }
+
+ public void setJavaShape(Shape shape) {
+ reset();
+ mPath.append(shape, false /*connect*/);
+ }
+
+ public void reset() {
+ mPath.reset();
+ mLastX = 0;
+ mLastY = 0;
+ }
+
+ public void setPathIterator(PathIterator iterator) {
+ reset();
+ mPath.append(iterator, false /*connect*/);
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nInit() {
+ // create the delegate
+ Path_Delegate newDelegate = new Path_Delegate();
+
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nInit(long nPath) {
+ // create the delegate
+ Path_Delegate newDelegate = new Path_Delegate();
+
+ // get the delegate to copy, which could be null if nPath is 0
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate != null) {
+ newDelegate.set(pathDelegate);
+ }
+
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nReset(long nPath) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.reset();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nRewind(long nPath) {
+ // call out to reset since there's nothing to optimize in
+ // terms of data structs.
+ nReset(nPath);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSet(long native_dst, long nSrc) {
+ Path_Delegate pathDstDelegate = sManager.getDelegate(native_dst);
+ if (pathDstDelegate == null) {
+ return;
+ }
+
+ Path_Delegate pathSrcDelegate = sManager.getDelegate(nSrc);
+ if (pathSrcDelegate == null) {
+ return;
+ }
+
+ pathDstDelegate.set(pathSrcDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nIsConvex(long nPath) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "Path.isConvex is not supported.", null, null);
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nGetFillType(long nPath) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return 0;
+ }
+
+ return pathDelegate.mFillType.nativeInt;
+ }
+
+ @LayoutlibDelegate
+ public static void nSetFillType(long nPath, int ft) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.setFillType(Path.sFillTypeArray[ft]);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nIsEmpty(long nPath) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ return pathDelegate == null || pathDelegate.isEmpty();
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nIsRect(long nPath, RectF rect) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return false;
+ }
+
+ // create an Area that can test if the path is a rect
+ Area area = new Area(pathDelegate.mPath);
+ if (area.isRectangular()) {
+ if (rect != null) {
+ pathDelegate.fillBounds(rect);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nComputeBounds(long nPath, RectF bounds) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.fillBounds(bounds);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nIncReserve(long nPath, int extraPtCount) {
+ // since we use a java2D path, there's no way to pre-allocate new points,
+ // so we do nothing.
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nMoveTo(long nPath, float x, float y) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.moveTo(x, y);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nRMoveTo(long nPath, float dx, float dy) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.rMoveTo(dx, dy);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nLineTo(long nPath, float x, float y) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.lineTo(x, y);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nRLineTo(long nPath, float dx, float dy) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.rLineTo(dx, dy);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nQuadTo(long nPath, float x1, float y1, float x2, float y2) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.quadTo(x1, y1, x2, y2);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nRQuadTo(long nPath, float dx1, float dy1, float dx2, float dy2) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.rQuadTo(dx1, dy1, dx2, dy2);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nCubicTo(long nPath, float x1, float y1,
+ float x2, float y2, float x3, float y3) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.cubicTo(x1, y1, x2, y2, x3, y3);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nRCubicTo(long nPath, float x1, float y1,
+ float x2, float y2, float x3, float y3) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.rCubicTo(x1, y1, x2, y2, x3, y3);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nArcTo(long nPath, float left, float top, float right,
+ float bottom,
+ float startAngle, float sweepAngle, boolean forceMoveTo) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.arcTo(left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nClose(long nPath) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.close();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAddRect(long nPath,
+ float left, float top, float right, float bottom, int dir) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.addRect(left, top, right, bottom, dir);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAddOval(long nPath, float left, float top, float right,
+ float bottom, int dir) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.mPath.append(new Ellipse2D.Float(
+ left, top, right - left, bottom - top), false);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAddCircle(long nPath, float x, float y, float radius, int dir) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ // because x/y is the center of the circle, need to offset this by the radius
+ pathDelegate.mPath.append(new Ellipse2D.Float(
+ x - radius, y - radius, radius * 2, radius * 2), false);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAddArc(long nPath, float left, float top, float right,
+ float bottom, float startAngle, float sweepAngle) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ // because x/y is the center of the circle, need to offset this by the radius
+ pathDelegate.mPath.append(new Arc2D.Float(
+ left, top, right - left, bottom - top,
+ -startAngle, -sweepAngle, Arc2D.OPEN), false);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAddRoundRect(long nPath, float left, float top, float right,
+ float bottom, float rx, float ry, int dir) {
+
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.mPath.append(new RoundRectangle2D.Float(
+ left, top, right - left, bottom - top, rx * 2, ry * 2), false);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAddRoundRect(long nPath, float left, float top, float right,
+ float bottom, float[] radii, int dir) {
+
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ float[] cornerDimensions = new float[radii.length];
+ for (int i = 0; i < radii.length; i++) {
+ cornerDimensions[i] = 2 * radii[i];
+ }
+ pathDelegate.mPath.append(new RoundRectangle(left, top, right - left, bottom - top,
+ cornerDimensions), false);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAddPath(long nPath, long src, float dx, float dy) {
+ addPath(nPath, src, AffineTransform.getTranslateInstance(dx, dy));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAddPath(long nPath, long src) {
+ addPath(nPath, src, null /*transform*/);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nAddPath(long nPath, long src, long matrix) {
+ Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix);
+ if (matrixDelegate == null) {
+ return;
+ }
+
+ addPath(nPath, src, matrixDelegate.getAffineTransform());
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nOffset(long nPath, float dx, float dy) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.offset(dx, dy);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetLastPoint(long nPath, float dx, float dy) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ pathDelegate.mLastX = dx;
+ pathDelegate.mLastY = dy;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nTransform(long nPath, long matrix,
+ long dst_path) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return;
+ }
+
+ Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix);
+ if (matrixDelegate == null) {
+ return;
+ }
+
+ // this can be null if dst_path is 0
+ Path_Delegate dstDelegate = sManager.getDelegate(dst_path);
+
+ pathDelegate.transform(matrixDelegate, dstDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nTransform(long nPath, long matrix) {
+ nTransform(nPath, matrix, 0);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nOp(long nPath1, long nPath2, int op, long result) {
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, "Path.op() not supported", null);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nFinalize(long nPath) {
+ sManager.removeJavaReferenceFor(nPath);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static float[] nApproximate(long nPath, float error) {
+ Path_Delegate pathDelegate = sManager.getDelegate(nPath);
+ if (pathDelegate == null) {
+ return null;
+ }
+ // Get a FlatteningIterator
+ PathIterator iterator = pathDelegate.getJavaShape().getPathIterator(null, error);
+
+ float segment[] = new float[6];
+ float totalLength = 0;
+ ArrayList<Point2D.Float> points = new ArrayList<Point2D.Float>();
+ Point2D.Float previousPoint = null;
+ while (!iterator.isDone()) {
+ int type = iterator.currentSegment(segment);
+ Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]);
+ // MoveTo shouldn't affect the length
+ if (previousPoint != null && type != PathIterator.SEG_MOVETO) {
+ totalLength += currentPoint.distance(previousPoint);
+ }
+ previousPoint = currentPoint;
+ points.add(currentPoint);
+ iterator.next();
+ }
+
+ int nPoints = points.size();
+ float[] result = new float[nPoints * 3];
+ previousPoint = null;
+ // Distance that we've covered so far. Used to calculate the fraction of the path that
+ // we've covered up to this point.
+ float walkedDistance = .0f;
+ for (int i = 0; i < nPoints; i++) {
+ Point2D.Float point = points.get(i);
+ float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f;
+ walkedDistance += distance;
+ result[i * 3] = walkedDistance / totalLength;
+ result[i * 3 + 1] = point.x;
+ result[i * 3 + 2] = point.y;
+
+ previousPoint = point;
+ }
+
+ return result;
+ }
+
+ // ---- Private helper methods ----
+
+ private void set(Path_Delegate delegate) {
+ mPath.reset();
+ setFillType(delegate.mFillType);
+ mPath.append(delegate.mPath, false /*connect*/);
+ }
+
+ private void setFillType(FillType fillType) {
+ mFillType = fillType;
+ mPath.setWindingRule(getWindingRule(fillType));
+ }
+
+ /**
+ * Returns the Java2D winding rules matching a given Android {@link FillType}.
+ * @param type the android fill type
+ * @return the matching java2d winding rule.
+ */
+ private static int getWindingRule(FillType type) {
+ switch (type) {
+ case WINDING:
+ case INVERSE_WINDING:
+ return GeneralPath.WIND_NON_ZERO;
+ case EVEN_ODD:
+ case INVERSE_EVEN_ODD:
+ return GeneralPath.WIND_EVEN_ODD;
+
+ default:
+ assert false;
+ return GeneralPath.WIND_NON_ZERO;
+ }
+ }
+
+ @NonNull
+ private static Direction getDirection(int direction) {
+ for (Direction d : Direction.values()) {
+ if (direction == d.nativeInt) {
+ return d;
+ }
+ }
+
+ assert false;
+ return null;
+ }
+
+ public static void addPath(long destPath, long srcPath, AffineTransform transform) {
+ Path_Delegate destPathDelegate = sManager.getDelegate(destPath);
+ if (destPathDelegate == null) {
+ return;
+ }
+
+ Path_Delegate srcPathDelegate = sManager.getDelegate(srcPath);
+ if (srcPathDelegate == null) {
+ return;
+ }
+
+ if (transform != null) {
+ destPathDelegate.mPath.append(
+ srcPathDelegate.mPath.getPathIterator(transform), false);
+ } else {
+ destPathDelegate.mPath.append(srcPathDelegate.mPath, false);
+ }
+ }
+
+
+ /**
+ * Returns whether the path already contains any points.
+ * Note that this is different to
+ * {@link #isEmpty} because if all elements are {@link PathIterator#SEG_MOVETO},
+ * {@link #isEmpty} will return true while hasPoints will return false.
+ */
+ public boolean hasPoints() {
+ return !mPath.getPathIterator(null).isDone();
+ }
+
+ /**
+ * Returns whether the path is empty (contains no lines or curves).
+ * @see Path#isEmpty
+ */
+ public boolean isEmpty() {
+ if (!mCachedIsEmpty) {
+ return false;
+ }
+
+ float[] coords = new float[6];
+ mCachedIsEmpty = Boolean.TRUE;
+ for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) {
+ int type = it.currentSegment(coords);
+ if (type != PathIterator.SEG_MOVETO) {
+ // Once we know that the path is not empty, we do not need to check again unless
+ // Path#reset is called.
+ mCachedIsEmpty = false;
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Fills the given {@link RectF} with the path bounds.
+ * @param bounds the RectF to be filled.
+ */
+ public void fillBounds(RectF bounds) {
+ Rectangle2D rect = mPath.getBounds2D();
+ bounds.left = (float)rect.getMinX();
+ bounds.right = (float)rect.getMaxX();
+ bounds.top = (float)rect.getMinY();
+ bounds.bottom = (float)rect.getMaxY();
+ }
+
+ /**
+ * Set the beginning of the next contour to the point (x,y).
+ *
+ * @param x The x-coordinate of the start of a new contour
+ * @param y The y-coordinate of the start of a new contour
+ */
+ public void moveTo(float x, float y) {
+ mPath.moveTo(mLastX = x, mLastY = y);
+ }
+
+ /**
+ * Set the beginning of the next contour relative to the last point on the
+ * previous contour. If there is no previous contour, this is treated the
+ * same as moveTo().
+ *
+ * @param dx The amount to add to the x-coordinate of the end of the
+ * previous contour, to specify the start of a new contour
+ * @param dy The amount to add to the y-coordinate of the end of the
+ * previous contour, to specify the start of a new contour
+ */
+ public void rMoveTo(float dx, float dy) {
+ dx += mLastX;
+ dy += mLastY;
+ mPath.moveTo(mLastX = dx, mLastY = dy);
+ }
+
+ /**
+ * Add a line from the last point to the specified point (x,y).
+ * If no moveTo() call has been made for this contour, the first point is
+ * automatically set to (0,0).
+ *
+ * @param x The x-coordinate of the end of a line
+ * @param y The y-coordinate of the end of a line
+ */
+ public void lineTo(float x, float y) {
+ if (!hasPoints()) {
+ mPath.moveTo(mLastX = 0, mLastY = 0);
+ }
+ mPath.lineTo(mLastX = x, mLastY = y);
+ }
+
+ /**
+ * Same as lineTo, but the coordinates are considered relative to the last
+ * point on this contour. If there is no previous point, then a moveTo(0,0)
+ * is inserted automatically.
+ *
+ * @param dx The amount to add to the x-coordinate of the previous point on
+ * this contour, to specify a line
+ * @param dy The amount to add to the y-coordinate of the previous point on
+ * this contour, to specify a line
+ */
+ public void rLineTo(float dx, float dy) {
+ if (!hasPoints()) {
+ mPath.moveTo(mLastX = 0, mLastY = 0);
+ }
+
+ if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
+ // The delta is so small that this shouldn't generate a line
+ return;
+ }
+
+ dx += mLastX;
+ dy += mLastY;
+ mPath.lineTo(mLastX = dx, mLastY = dy);
+ }
+
+ /**
+ * Add a quadratic bezier from the last point, approaching control point
+ * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
+ * this contour, the first point is automatically set to (0,0).
+ *
+ * @param x1 The x-coordinate of the control point on a quadratic curve
+ * @param y1 The y-coordinate of the control point on a quadratic curve
+ * @param x2 The x-coordinate of the end point on a quadratic curve
+ * @param y2 The y-coordinate of the end point on a quadratic curve
+ */
+ public void quadTo(float x1, float y1, float x2, float y2) {
+ mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2);
+ }
+
+ /**
+ * Same as quadTo, but the coordinates are considered relative to the last
+ * point on this contour. If there is no previous point, then a moveTo(0,0)
+ * is inserted automatically.
+ *
+ * @param dx1 The amount to add to the x-coordinate of the last point on
+ * this contour, for the control point of a quadratic curve
+ * @param dy1 The amount to add to the y-coordinate of the last point on
+ * this contour, for the control point of a quadratic curve
+ * @param dx2 The amount to add to the x-coordinate of the last point on
+ * this contour, for the end point of a quadratic curve
+ * @param dy2 The amount to add to the y-coordinate of the last point on
+ * this contour, for the end point of a quadratic curve
+ */
+ public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
+ if (!hasPoints()) {
+ mPath.moveTo(mLastX = 0, mLastY = 0);
+ }
+ dx1 += mLastX;
+ dy1 += mLastY;
+ dx2 += mLastX;
+ dy2 += mLastY;
+ mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2);
+ }
+
+ /**
+ * Add a cubic bezier from the last point, approaching control points
+ * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
+ * made for this contour, the first point is automatically set to (0,0).
+ *
+ * @param x1 The x-coordinate of the 1st control point on a cubic curve
+ * @param y1 The y-coordinate of the 1st control point on a cubic curve
+ * @param x2 The x-coordinate of the 2nd control point on a cubic curve
+ * @param y2 The y-coordinate of the 2nd control point on a cubic curve
+ * @param x3 The x-coordinate of the end point on a cubic curve
+ * @param y3 The y-coordinate of the end point on a cubic curve
+ */
+ public void cubicTo(float x1, float y1, float x2, float y2,
+ float x3, float y3) {
+ if (!hasPoints()) {
+ mPath.moveTo(0, 0);
+ }
+ mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
+ }
+
+ /**
+ * Same as cubicTo, but the coordinates are considered relative to the
+ * current point on this contour. If there is no previous point, then a
+ * moveTo(0,0) is inserted automatically.
+ */
+ public void rCubicTo(float dx1, float dy1, float dx2, float dy2,
+ float dx3, float dy3) {
+ if (!hasPoints()) {
+ mPath.moveTo(mLastX = 0, mLastY = 0);
+ }
+ dx1 += mLastX;
+ dy1 += mLastY;
+ dx2 += mLastX;
+ dy2 += mLastY;
+ dx3 += mLastX;
+ dy3 += mLastY;
+ mPath.curveTo(dx1, dy1, dx2, dy2, mLastX = dx3, mLastY = dy3);
+ }
+
+ /**
+ * Append the specified arc to the path as a new contour. If the start of
+ * the path is different from the path's current last point, then an
+ * automatic lineTo() is added to connect the current contour to the
+ * start of the arc. However, if the path is empty, then we call moveTo()
+ * with the first point of the arc. The sweep angle is tread mod 360.
+ *
+ * @param left The left of oval defining shape and size of the arc
+ * @param top The top of oval defining shape and size of the arc
+ * @param right The right of oval defining shape and size of the arc
+ * @param bottom The bottom of oval defining shape and size of the arc
+ * @param startAngle Starting angle (in degrees) where the arc begins
+ * @param sweepAngle Sweep angle (in degrees) measured clockwise, treated
+ * mod 360.
+ * @param forceMoveTo If true, always begin a new contour with the arc
+ */
+ public void arcTo(float left, float top, float right, float bottom, float startAngle,
+ float sweepAngle,
+ boolean forceMoveTo) {
+ Arc2D arc = new Arc2D.Float(left, top, right - left, bottom - top, -startAngle,
+ -sweepAngle, Arc2D.OPEN);
+ mPath.append(arc, true /*connect*/);
+
+ resetLastPointFromPath();
+ }
+
+ /**
+ * Close the current contour. If the current point is not equal to the
+ * first point of the contour, a line segment is automatically added.
+ */
+ public void close() {
+ mPath.closePath();
+ }
+
+ private void resetLastPointFromPath() {
+ Point2D last = mPath.getCurrentPoint();
+ mLastX = (float) last.getX();
+ mLastY = (float) last.getY();
+ }
+
+ /**
+ * Add a closed rectangle contour to the path
+ *
+ * @param left The left side of a rectangle to add to the path
+ * @param top The top of a rectangle to add to the path
+ * @param right The right side of a rectangle to add to the path
+ * @param bottom The bottom of a rectangle to add to the path
+ * @param dir The direction to wind the rectangle's contour
+ */
+ public void addRect(float left, float top, float right, float bottom,
+ int dir) {
+ moveTo(left, top);
+
+ Direction direction = getDirection(dir);
+
+ switch (direction) {
+ case CW:
+ lineTo(right, top);
+ lineTo(right, bottom);
+ lineTo(left, bottom);
+ break;
+ case CCW:
+ lineTo(left, bottom);
+ lineTo(right, bottom);
+ lineTo(right, top);
+ break;
+ }
+
+ close();
+
+ resetLastPointFromPath();
+ }
+
+ /**
+ * Offset the path by (dx,dy), returning true on success
+ *
+ * @param dx The amount in the X direction to offset the entire path
+ * @param dy The amount in the Y direction to offset the entire path
+ */
+ public void offset(float dx, float dy) {
+ GeneralPath newPath = new GeneralPath();
+
+ PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy));
+
+ newPath.append(iterator, false /*connect*/);
+ mPath = newPath;
+ }
+
+ /**
+ * Transform the points in this path by matrix, and write the answer
+ * into dst. If dst is null, then the the original path is modified.
+ *
+ * @param matrix The matrix to apply to the path
+ * @param dst The transformed path is written here. If dst is null,
+ * then the the original path is modified
+ */
+ public void transform(Matrix_Delegate matrix, Path_Delegate dst) {
+ if (matrix.hasPerspective()) {
+ assert false;
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_AFFINE,
+ "android.graphics.Path#transform() only " +
+ "supports affine transformations.", null, null /*data*/);
+ }
+
+ GeneralPath newPath = new GeneralPath();
+
+ PathIterator iterator = mPath.getPathIterator(matrix.getAffineTransform());
+
+ newPath.append(iterator, false /*connect*/);
+
+ if (dst != null) {
+ dst.mPath = newPath;
+ } else {
+ mPath = newPath;
+ }
+ }
+}
diff --git a/android/graphics/Picture.java b/android/graphics/Picture.java
new file mode 100644
index 0000000..08eeaff
--- /dev/null
+++ b/android/graphics/Picture.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A Picture records drawing calls (via the canvas returned by beginRecording)
+ * and can then play them back into Canvas (via {@link Picture#draw(Canvas)} or
+ * {@link Canvas#drawPicture(Picture)}).For most content (e.g. text, lines, rectangles),
+ * drawing a sequence from a picture can be faster than the equivalent API
+ * calls, since the picture performs its playback without incurring any
+ * method-call overhead.
+ *
+ * <p class="note"><strong>Note:</strong> Prior to API level 23 a picture cannot
+ * be replayed on a hardware accelerated canvas.</p>
+ */
+public class Picture {
+ private Canvas mRecordingCanvas;
+ private long mNativePicture;
+
+ private static final int WORKING_STREAM_STORAGE = 16 * 1024;
+
+ /**
+ * Creates an empty picture that is ready to record.
+ */
+ public Picture() {
+ this(nativeConstructor(0));
+ }
+
+ /**
+ * Create a picture by making a copy of what has already been recorded in
+ * src. The contents of src are unchanged, and if src changes later, those
+ * changes will not be reflected in this picture.
+ */
+ public Picture(Picture src) {
+ this(nativeConstructor(src != null ? src.mNativePicture : 0));
+ }
+
+ private Picture(long nativePicture) {
+ if (nativePicture == 0) {
+ throw new RuntimeException();
+ }
+ mNativePicture = nativePicture;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ nativeDestructor(mNativePicture);
+ mNativePicture = 0;
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * To record a picture, call beginRecording() and then draw into the Canvas
+ * that is returned. Nothing we appear on screen, but all of the draw
+ * commands (e.g. {@link Canvas#drawRect(Rect, Paint)}) will be recorded.
+ * To stop recording, call endRecording(). After endRecording() the Canvas
+ * that was returned must no longer be used, and nothing should be drawn
+ * into it.
+ */
+ public Canvas beginRecording(int width, int height) {
+ long ni = nativeBeginRecording(mNativePicture, width, height);
+ mRecordingCanvas = new RecordingCanvas(this, ni);
+ return mRecordingCanvas;
+ }
+
+ /**
+ * Call endRecording when the picture is built. After this call, the picture
+ * may be drawn, but the canvas that was returned by beginRecording must not
+ * be used anymore. This is automatically called if {@link Picture#draw}
+ * or {@link Canvas#drawPicture(Picture)} is called.
+ */
+ public void endRecording() {
+ if (mRecordingCanvas != null) {
+ mRecordingCanvas = null;
+ nativeEndRecording(mNativePicture);
+ }
+ }
+
+ /**
+ * Get the width of the picture as passed to beginRecording. This
+ * does not reflect (per se) the content of the picture.
+ */
+ public int getWidth() {
+ return nativeGetWidth(mNativePicture);
+ }
+
+ /**
+ * Get the height of the picture as passed to beginRecording. This
+ * does not reflect (per se) the content of the picture.
+ */
+ public int getHeight() {
+ return nativeGetHeight(mNativePicture);
+ }
+
+ /**
+ * Draw this picture on the canvas.
+ * <p>
+ * Prior to {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this call could
+ * have the side effect of changing the matrix and clip of the canvas
+ * if this picture had imbalanced saves/restores.
+ *
+ * <p>
+ * <strong>Note:</strong> This forces the picture to internally call
+ * {@link Picture#endRecording()} in order to prepare for playback.
+ *
+ * @param canvas The picture is drawn to this canvas
+ */
+ public void draw(Canvas canvas) {
+ if (mRecordingCanvas != null) {
+ endRecording();
+ }
+ nativeDraw(canvas.getNativeCanvasWrapper(), mNativePicture);
+ }
+
+ /**
+ * Create a new picture (already recorded) from the data in the stream. This
+ * data was generated by a previous call to writeToStream(). Pictures that
+ * have been persisted across device restarts are not guaranteed to decode
+ * properly and are highly discouraged.
+ *
+ * @see #writeToStream(java.io.OutputStream)
+ * @deprecated The recommended alternative is to not use writeToStream and
+ * instead draw the picture into a Bitmap from which you can persist it as
+ * raw or compressed pixels.
+ */
+ @Deprecated
+ public static Picture createFromStream(InputStream stream) {
+ return new Picture(nativeCreateFromStream(stream, new byte[WORKING_STREAM_STORAGE]));
+ }
+
+ /**
+ * Write the picture contents to a stream. The data can be used to recreate
+ * the picture in this or another process by calling createFromStream(...)
+ * The resulting stream is NOT to be persisted across device restarts as
+ * there is no guarantee that the Picture can be successfully reconstructed.
+ *
+ * @see #createFromStream(java.io.InputStream)
+ * @deprecated The recommended alternative is to draw the picture into a
+ * Bitmap from which you can persist it as raw or compressed pixels.
+ */
+ @Deprecated
+ public void writeToStream(OutputStream stream) {
+ // do explicit check before calling the native method
+ if (stream == null) {
+ throw new NullPointerException();
+ }
+ if (!nativeWriteToStream(mNativePicture, stream,
+ new byte[WORKING_STREAM_STORAGE])) {
+ throw new RuntimeException();
+ }
+ }
+
+ // return empty picture if src is 0, or a copy of the native src
+ private static native long nativeConstructor(long nativeSrcOr0);
+ private static native long nativeCreateFromStream(InputStream stream, byte[] storage);
+ private static native int nativeGetWidth(long nativePicture);
+ private static native int nativeGetHeight(long nativePicture);
+ private static native long nativeBeginRecording(long nativeCanvas, int w, int h);
+ private static native void nativeEndRecording(long nativeCanvas);
+ private static native void nativeDraw(long nativeCanvas, long nativePicture);
+ private static native boolean nativeWriteToStream(long nativePicture,
+ OutputStream stream, byte[] storage);
+ private static native void nativeDestructor(long nativePicture);
+
+ private static class RecordingCanvas extends Canvas {
+ private final Picture mPicture;
+
+ public RecordingCanvas(Picture pict, long nativeCanvas) {
+ super(nativeCanvas);
+ mPicture = pict;
+ }
+
+ @Override
+ public void setBitmap(Bitmap bitmap) {
+ throw new RuntimeException("Cannot call setBitmap on a picture canvas");
+ }
+
+ @Override
+ public void drawPicture(Picture picture) {
+ if (mPicture == picture) {
+ throw new RuntimeException("Cannot draw a picture into its recording canvas");
+ }
+ super.drawPicture(picture);
+ }
+ }
+}
diff --git a/android/graphics/PixelFormat.java b/android/graphics/PixelFormat.java
new file mode 100644
index 0000000..f93886d
--- /dev/null
+++ b/android/graphics/PixelFormat.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+public class PixelFormat {
+ /** @hide */
+ @IntDef({UNKNOWN, TRANSLUCENT, TRANSPARENT, OPAQUE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Opacity {}
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({RGBA_8888, RGBX_8888, RGBA_F16, RGBA_1010102, RGB_888, RGB_565})
+ public @interface Format { }
+
+ // NOTE: these constants must match the values from graphics/common/x.x/types.hal
+
+ public static final int UNKNOWN = 0;
+
+ /** System chooses a format that supports translucency (many alpha bits) */
+ public static final int TRANSLUCENT = -3;
+
+ /**
+ * System chooses a format that supports transparency
+ * (at least 1 alpha bit)
+ */
+ public static final int TRANSPARENT = -2;
+
+ /** System chooses an opaque format (no alpha bits required) */
+ public static final int OPAQUE = -1;
+
+ public static final int RGBA_8888 = 1;
+ public static final int RGBX_8888 = 2;
+ public static final int RGB_888 = 3;
+ public static final int RGB_565 = 4;
+
+ @Deprecated
+ public static final int RGBA_5551 = 6;
+ @Deprecated
+ public static final int RGBA_4444 = 7;
+ @Deprecated
+ public static final int A_8 = 8;
+ @Deprecated
+ public static final int L_8 = 9;
+ @Deprecated
+ public static final int LA_88 = 0xA;
+ @Deprecated
+ public static final int RGB_332 = 0xB;
+
+ /**
+ * @deprecated use {@link android.graphics.ImageFormat#NV16
+ * ImageFormat.NV16} instead.
+ */
+ @Deprecated
+ public static final int YCbCr_422_SP = 0x10;
+
+ /**
+ * @deprecated use {@link android.graphics.ImageFormat#NV21
+ * ImageFormat.NV21} instead.
+ */
+ @Deprecated
+ public static final int YCbCr_420_SP = 0x11;
+
+ /**
+ * @deprecated use {@link android.graphics.ImageFormat#YUY2
+ * ImageFormat.YUY2} instead.
+ */
+ @Deprecated
+ public static final int YCbCr_422_I = 0x14;
+
+ public static final int RGBA_F16 = 0x16;
+ public static final int RGBA_1010102 = 0x2B;
+
+ /**
+ * @deprecated use {@link android.graphics.ImageFormat#JPEG
+ * ImageFormat.JPEG} instead.
+ */
+ @Deprecated
+ public static final int JPEG = 0x100;
+
+ public int bytesPerPixel;
+ public int bitsPerPixel;
+
+ public static void getPixelFormatInfo(@Format int format, PixelFormat info) {
+ switch (format) {
+ case RGBA_8888:
+ case RGBX_8888:
+ case RGBA_1010102:
+ info.bitsPerPixel = 32;
+ info.bytesPerPixel = 4;
+ break;
+ case RGB_888:
+ info.bitsPerPixel = 24;
+ info.bytesPerPixel = 3;
+ break;
+ case RGB_565:
+ case RGBA_5551:
+ case RGBA_4444:
+ case LA_88:
+ info.bitsPerPixel = 16;
+ info.bytesPerPixel = 2;
+ break;
+ case A_8:
+ case L_8:
+ case RGB_332:
+ info.bitsPerPixel = 8;
+ info.bytesPerPixel = 1;
+ break;
+ case YCbCr_422_SP:
+ case YCbCr_422_I:
+ info.bitsPerPixel = 16;
+ info.bytesPerPixel = 1;
+ break;
+ case YCbCr_420_SP:
+ info.bitsPerPixel = 12;
+ info.bytesPerPixel = 1;
+ break;
+ case RGBA_F16:
+ info.bitsPerPixel = 64;
+ info.bytesPerPixel = 8;
+ break;
+ default:
+ throw new IllegalArgumentException("unknown pixel format " + format);
+ }
+ }
+
+ public static boolean formatHasAlpha(@Format int format) {
+ switch (format) {
+ case PixelFormat.A_8:
+ case PixelFormat.LA_88:
+ case PixelFormat.RGBA_4444:
+ case PixelFormat.RGBA_5551:
+ case PixelFormat.RGBA_8888:
+ case PixelFormat.RGBA_F16:
+ case PixelFormat.RGBA_1010102:
+ case PixelFormat.TRANSLUCENT:
+ case PixelFormat.TRANSPARENT:
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Determine whether or not this is a public-visible and non-deprecated {@code format}.
+ *
+ * <p>In particular, {@code @hide} formats will return {@code false}.</p>
+ *
+ * <p>Any other indirect formats (such as {@code TRANSPARENT} or {@code TRANSLUCENT})
+ * will return {@code false}.</p>
+ *
+ * @param format an integer format
+ * @return a boolean
+ *
+ * @hide
+ */
+ public static boolean isPublicFormat(@Format int format) {
+ switch (format) {
+ case RGBA_8888:
+ case RGBX_8888:
+ case RGB_888:
+ case RGB_565:
+ case RGBA_F16:
+ case RGBA_1010102:
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/android/graphics/PixelXorXfermode.java b/android/graphics/PixelXorXfermode.java
new file mode 100644
index 0000000..27884e0
--- /dev/null
+++ b/android/graphics/PixelXorXfermode.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+/**
+ * @removed
+ */
+@Deprecated
+public class PixelXorXfermode extends Xfermode {
+
+ public PixelXorXfermode(int opColor) {
+ }
+}
diff --git a/android/graphics/Point.java b/android/graphics/Point.java
new file mode 100644
index 0000000..abcccbd
--- /dev/null
+++ b/android/graphics/Point.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.PrintWriter;
+
+
+/**
+ * Point holds two integer coordinates
+ */
+public class Point implements Parcelable {
+ public int x;
+ public int y;
+
+ public Point() {}
+
+ public Point(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public Point(Point src) {
+ this.x = src.x;
+ this.y = src.y;
+ }
+
+ /**
+ * Set the point's x and y coordinates
+ */
+ public void set(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Negate the point's coordinates
+ */
+ public final void negate() {
+ x = -x;
+ y = -y;
+ }
+
+ /**
+ * Offset the point's coordinates by dx, dy
+ */
+ public final void offset(int dx, int dy) {
+ x += dx;
+ y += dy;
+ }
+
+ /**
+ * Returns true if the point's coordinates equal (x,y)
+ */
+ public final boolean equals(int x, int y) {
+ return this.x == x && this.y == y;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Point point = (Point) o;
+
+ if (x != point.x) return false;
+ if (y != point.y) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = x;
+ result = 31 * result + y;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Point(" + x + ", " + y + ")";
+ }
+
+ /** @hide */
+ public void printShortString(PrintWriter pw) {
+ pw.print("["); pw.print(x); pw.print(","); pw.print(y); pw.print("]");
+ }
+
+ /**
+ * Parcelable interface methods
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Write this point to the specified parcel. To restore a point from
+ * a parcel, use readFromParcel()
+ * @param out The parcel to write the point's coordinates into
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(x);
+ out.writeInt(y);
+ }
+
+ public static final Parcelable.Creator<Point> CREATOR = new Parcelable.Creator<Point>() {
+ /**
+ * Return a new point from the data in the specified parcel.
+ */
+ public Point createFromParcel(Parcel in) {
+ Point r = new Point();
+ r.readFromParcel(in);
+ return r;
+ }
+
+ /**
+ * Return an array of rectangles of the specified size.
+ */
+ public Point[] newArray(int size) {
+ return new Point[size];
+ }
+ };
+
+ /**
+ * Set the point's coordinates from the data stored in the specified
+ * parcel. To write a point to a parcel, call writeToParcel().
+ *
+ * @param in The parcel to read the point's coordinates from
+ */
+ public void readFromParcel(Parcel in) {
+ x = in.readInt();
+ y = in.readInt();
+ }
+}
diff --git a/android/graphics/PointF.java b/android/graphics/PointF.java
new file mode 100644
index 0000000..8e4288e
--- /dev/null
+++ b/android/graphics/PointF.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+/**
+ * PointF holds two float coordinates
+ */
+public class PointF implements Parcelable {
+ public float x;
+ public float y;
+
+ public PointF() {}
+
+ public PointF(float x, float y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public PointF(Point p) {
+ this.x = p.x;
+ this.y = p.y;
+ }
+
+ /**
+ * Set the point's x and y coordinates
+ */
+ public final void set(float x, float y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Set the point's x and y coordinates to the coordinates of p
+ */
+ public final void set(PointF p) {
+ this.x = p.x;
+ this.y = p.y;
+ }
+
+ public final void negate() {
+ x = -x;
+ y = -y;
+ }
+
+ public final void offset(float dx, float dy) {
+ x += dx;
+ y += dy;
+ }
+
+ /**
+ * Returns true if the point's coordinates equal (x,y)
+ */
+ public final boolean equals(float x, float y) {
+ return this.x == x && this.y == y;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PointF pointF = (PointF) o;
+
+ if (Float.compare(pointF.x, x) != 0) return false;
+ if (Float.compare(pointF.y, y) != 0) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (x != +0.0f ? Float.floatToIntBits(x) : 0);
+ result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "PointF(" + x + ", " + y + ")";
+ }
+
+ /**
+ * Return the euclidian distance from (0,0) to the point
+ */
+ public final float length() {
+ return length(x, y);
+ }
+
+ /**
+ * Returns the euclidian distance from (0,0) to (x,y)
+ */
+ public static float length(float x, float y) {
+ return (float) Math.hypot(x, y);
+ }
+
+ /**
+ * Parcelable interface methods
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Write this point to the specified parcel. To restore a point from
+ * a parcel, use readFromParcel()
+ * @param out The parcel to write the point's coordinates into
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeFloat(x);
+ out.writeFloat(y);
+ }
+
+ public static final Parcelable.Creator<PointF> CREATOR = new Parcelable.Creator<PointF>() {
+ /**
+ * Return a new point from the data in the specified parcel.
+ */
+ public PointF createFromParcel(Parcel in) {
+ PointF r = new PointF();
+ r.readFromParcel(in);
+ return r;
+ }
+
+ /**
+ * Return an array of rectangles of the specified size.
+ */
+ public PointF[] newArray(int size) {
+ return new PointF[size];
+ }
+ };
+
+ /**
+ * Set the point's coordinates from the data stored in the specified
+ * parcel. To write a point to a parcel, call writeToParcel().
+ *
+ * @param in The parcel to read the point's coordinates from
+ */
+ public void readFromParcel(Parcel in) {
+ x = in.readFloat();
+ y = in.readFloat();
+ }
+}
diff --git a/android/graphics/PorterDuff.java b/android/graphics/PorterDuff.java
new file mode 100644
index 0000000..d7d3049
--- /dev/null
+++ b/android/graphics/PorterDuff.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * <p>This class contains the list of alpha compositing and blending modes
+ * that can be passed to {@link PorterDuffXfermode}, a specialized implementation
+ * of {@link Paint}'s {@link Paint#setXfermode(Xfermode) transfer mode}.
+ * All the available modes can be found in the {@link Mode} enum.</p>
+ */
+public class PorterDuff {
+ /**
+ * {@usesMathJax}
+ *
+ * <h3>Porter-Duff</h3>
+ *
+ * <p>The name of the parent class is an homage to the work of Thomas Porter and
+ * Tom Duff, presented in their seminal 1984 paper titled "Compositing Digital Images".
+ * In this paper, the authors describe 12 compositing operators that govern how to
+ * compute the color resulting of the composition of a source (the graphics object
+ * to render) with a destination (the content of the render target).</p>
+ *
+ * <p>"Compositing Digital Images" was published in <em>Computer Graphics</em>
+ * Volume 18, Number 3 dated July 1984.</p>
+ *
+ * <p>Because the work of Porter and Duff focuses solely on the effects of the alpha
+ * channel of the source and destination, the 12 operators described in the original
+ * paper are called alpha compositing modes here.</p>
+ *
+ * <p>For convenience, this class also provides several blending modes, which similarly
+ * define the result of compositing a source and a destination but without being
+ * constrained to the alpha channel. These blending modes are not defined by Porter
+ * and Duff but have been included in this class for convenience purposes.</p>
+ *
+ * <h3>Diagrams</h3>
+ *
+ * <p>All the example diagrams presented below use the same source and destination
+ * images:</p>
+ *
+ * <table summary="Source and Destination" style="background-color: transparent;">
+ * <tr>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC.png" />
+ * <figcaption>Source image</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST.png" />
+ * <figcaption>Destination image</figcaption>
+ * </td>
+ * </tr>
+ * </table>
+ *
+ * <p>The order of drawing operations used to generate each diagram is shown in the
+ * following code snippet:</p>
+ *
+ * <pre class="prettyprint">
+ * Paint paint = new Paint();
+ * canvas.drawBitmap(destinationImage, 0, 0, paint);
+ *
+ * PorterDuff.Mode mode = // choose a mode
+ * paint.setXfermode(new PorterDuffXfermode(mode));
+ *
+ * canvas.drawBitmap(sourceImage, 0, 0, paint);
+ * </pre>
+
+ *
+ * <h3>Alpha compositing modes</h3>
+ *
+ * <table summary="Alpha compositing modes" style="background-color: transparent;">
+ * <tr>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC.png" />
+ * <figcaption>{@link #SRC Source}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC_OVER.png" />
+ * <figcaption>{@link #SRC_OVER Source Over}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC_IN.png" />
+ * <figcaption>{@link #SRC_IN Source In}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC_ATOP.png" />
+ * <figcaption>{@link #SRC_ATOP Source Atop}</figcaption>
+ * </td>
+ * </tr>
+ * <tr>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST.png" />
+ * <figcaption>{@link #DST Destination}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST_OVER.png" />
+ * <figcaption>{@link #DST_OVER Destination Over}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST_IN.png" />
+ * <figcaption>{@link #DST_IN Destination In}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST_ATOP.png" />
+ * <figcaption>{@link #DST_ATOP Destination Atop}</figcaption>
+ * </td>
+ * </tr>
+ * <tr>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_CLEAR.png" />
+ * <figcaption>{@link #CLEAR Clear}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC_OUT.png" />
+ * <figcaption>{@link #SRC_OUT Source Out}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST_OUT.png" />
+ * <figcaption>{@link #DST_OUT Destination Out}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_XOR.png" />
+ * <figcaption>{@link #XOR Exclusive Or}</figcaption>
+ * </td>
+ * </tr>
+ * </table>
+ *
+ * <h3>Blending modes</h3>
+ *
+ * <table summary="Blending modes" style="background-color: transparent;">
+ * <tr>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DARKEN.png" />
+ * <figcaption>{@link #DARKEN Darken}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_LIGHTEN.png" />
+ * <figcaption>{@link #LIGHTEN Lighten}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_MULTIPLY.png" />
+ * <figcaption>{@link #MULTIPLY Multiply}</figcaption>
+ * </td>
+ * </tr>
+ * <tr>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SCREEN.png" />
+ * <figcaption>{@link #SCREEN Screen}</figcaption>
+ * </td>
+ * <td style="border: none; text-align: center;">
+ * <img src="{@docRoot}reference/android/images/graphics/composite_OVERLAY.png" />
+ * <figcaption>{@link #OVERLAY Overlay}</figcaption>
+ * </td>
+ * </tr>
+ * </table>
+ *
+ * <h3>Compositing equations</h3>
+ *
+ * <p>The documentation of each individual alpha compositing or blending mode below
+ * provides the exact equation used to compute alpha and color value of the result
+ * of the composition of a source and destination.</p>
+ *
+ * <p>The result (or output) alpha value is noted \(\alpha_{out}\). The result (or output)
+ * color value is noted \(C_{out}\).</p>
+ */
+ public enum Mode {
+ // these value must match their native equivalents. See SkXfermode.h
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_CLEAR.png" />
+ * <figcaption>Destination pixels covered by the source are cleared to 0.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = 0\)</p>
+ * <p>\(C_{out} = 0\)</p>
+ */
+ CLEAR (0),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC.png" />
+ * <figcaption>The source pixels replace the destination pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{src}\)</p>
+ * <p>\(C_{out} = C_{src}\)</p>
+ */
+ SRC (1),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST.png" />
+ * <figcaption>The source pixels are discarded, leaving the destination intact.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{dst}\)</p>
+ * <p>\(C_{out} = C_{dst}\)</p>
+ */
+ DST (2),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC_OVER.png" />
+ * <figcaption>The source pixels are drawn over the destination pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{src} + (1 - \alpha_{src}) * \alpha_{dst}\)</p>
+ * <p>\(C_{out} = C_{src} + (1 - \alpha_{src}) * C_{dst}\)</p>
+ */
+ SRC_OVER (3),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST_OVER.png" />
+ * <figcaption>The source pixels are drawn behind the destination pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{dst} + (1 - \alpha_{dst}) * \alpha_{src}\)</p>
+ * <p>\(C_{out} = C_{dst} + (1 - \alpha_{dst}) * C_{src}\)</p>
+ */
+ DST_OVER (4),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC_IN.png" />
+ * <figcaption>Keeps the source pixels that cover the destination pixels,
+ * discards the remaining source and destination pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{src} * \alpha_{dst}\)</p>
+ * <p>\(C_{out} = C_{src} * \alpha_{dst}\)</p>
+ */
+ SRC_IN (5),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST_IN.png" />
+ * <figcaption>Keeps the destination pixels that cover source pixels,
+ * discards the remaining source and destination pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{src} * \alpha_{dst}\)</p>
+ * <p>\(C_{out} = C_{dst} * \alpha_{src}\)</p>
+ */
+ DST_IN (6),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC_OUT.png" />
+ * <figcaption>Keeps the source pixels that do not cover destination pixels.
+ * Discards source pixels that cover destination pixels. Discards all
+ * destination pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = (1 - \alpha_{dst}) * \alpha_{src}\)</p>
+ * <p>\(C_{out} = (1 - \alpha_{dst}) * C_{src}\)</p>
+ */
+ SRC_OUT (7),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST_OUT.png" />
+ * <figcaption>Keeps the destination pixels that are not covered by source pixels.
+ * Discards destination pixels that are covered by source pixels. Discards all
+ * source pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = (1 - \alpha_{src}) * \alpha_{dst}\)</p>
+ * <p>\(C_{out} = (1 - \alpha_{src}) * C_{dst}\)</p>
+ */
+ DST_OUT (8),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SRC_ATOP.png" />
+ * <figcaption>Discards the source pixels that do not cover destination pixels.
+ * Draws remaining source pixels over destination pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{dst}\)</p>
+ * <p>\(C_{out} = \alpha_{dst} * C_{src} + (1 - \alpha_{src}) * C_{dst}\)</p>
+ */
+ SRC_ATOP (9),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DST_ATOP.png" />
+ * <figcaption>Discards the destination pixels that are not covered by source pixels.
+ * Draws remaining destination pixels over source pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{src}\)</p>
+ * <p>\(C_{out} = \alpha_{src} * C_{dst} + (1 - \alpha_{dst}) * C_{src}\)</p>
+ */
+ DST_ATOP (10),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_XOR.png" />
+ * <figcaption>Discards the source and destination pixels where source pixels
+ * cover destination pixels. Draws remaining source pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = (1 - \alpha_{dst}) * \alpha_{src} + (1 - \alpha_{src}) * \alpha_{dst}\)</p>
+ * <p>\(C_{out} = (1 - \alpha_{dst}) * C_{src} + (1 - \alpha_{src}) * C_{dst}\)</p>
+ */
+ XOR (11),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_DARKEN.png" />
+ * <figcaption>Retains the smallest component of the source and
+ * destination pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{src} + \alpha_{dst} - \alpha_{src} * \alpha_{dst}\)</p>
+ * <p>\(C_{out} = (1 - \alpha_{dst}) * C_{src} + (1 - \alpha_{src}) * C_{dst} + min(C_{src}, C_{dst})\)</p>
+ */
+ DARKEN (16),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_LIGHTEN.png" />
+ * <figcaption>Retains the largest component of the source and
+ * destination pixel.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{src} + \alpha_{dst} - \alpha_{src} * \alpha_{dst}\)</p>
+ * <p>\(C_{out} = (1 - \alpha_{dst}) * C_{src} + (1 - \alpha_{src}) * C_{dst} + max(C_{src}, C_{dst})\)</p>
+ */
+ LIGHTEN (17),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_MULTIPLY.png" />
+ * <figcaption>Multiplies the source and destination pixels.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{src} * \alpha_{dst}\)</p>
+ * <p>\(C_{out} = C_{src} * C_{dst}\)</p>
+ */
+ MULTIPLY (13),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_SCREEN.png" />
+ * <figcaption>Adds the source and destination pixels, then subtracts the
+ * source pixels multiplied by the destination.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{src} + \alpha_{dst} - \alpha_{src} * \alpha_{dst}\)</p>
+ * <p>\(C_{out} = C_{src} + C_{dst} - C_{src} * C_{dst}\)</p>
+ */
+ SCREEN (14),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_ADD.png" />
+ * <figcaption>Adds the source pixels to the destination pixels and saturates
+ * the result.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = max(0, min(\alpha_{src} + \alpha_{dst}, 1))\)</p>
+ * <p>\(C_{out} = max(0, min(C_{src} + C_{dst}, 1))\)</p>
+ */
+ ADD (12),
+ /**
+ * <p>
+ * <img src="{@docRoot}reference/android/images/graphics/composite_OVERLAY.png" />
+ * <figcaption>Multiplies or screens the source and destination depending on the
+ * destination color.</figcaption>
+ * </p>
+ * <p>\(\alpha_{out} = \alpha_{src} + \alpha_{dst} - \alpha_{src} * \alpha_{dst}\)</p>
+ * <p>\(\begin{equation}
+ * C_{out} = \begin{cases} 2 * C_{src} * C_{dst} & 2 * C_{dst} \lt \alpha_{dst} \\
+ * \alpha_{src} * \alpha_{dst} - 2 (\alpha_{dst} - C_{src}) (\alpha_{src} - C_{dst}) & otherwise \end{cases}
+ * \end{equation}\)</p>
+ */
+ OVERLAY (15);
+
+ Mode(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+
+ /**
+ * @hide
+ */
+ public final int nativeInt;
+ }
+
+ /**
+ * @hide
+ */
+ public static int modeToInt(Mode mode) {
+ return mode.nativeInt;
+ }
+
+ /**
+ * @hide
+ */
+ public static Mode intToMode(int val) {
+ switch (val) {
+ default:
+ case 0: return Mode.CLEAR;
+ case 1: return Mode.SRC;
+ case 2: return Mode.DST;
+ case 3: return Mode.SRC_OVER;
+ case 4: return Mode.DST_OVER;
+ case 5: return Mode.SRC_IN;
+ case 6: return Mode.DST_IN;
+ case 7: return Mode.SRC_OUT;
+ case 8: return Mode.DST_OUT;
+ case 9: return Mode.SRC_ATOP;
+ case 10: return Mode.DST_ATOP;
+ case 11: return Mode.XOR;
+ case 16: return Mode.DARKEN;
+ case 17: return Mode.LIGHTEN;
+ case 13: return Mode.MULTIPLY;
+ case 14: return Mode.SCREEN;
+ case 12: return Mode.ADD;
+ case 15: return Mode.OVERLAY;
+ }
+ }
+}
diff --git a/android/graphics/PorterDuffColorFilter.java b/android/graphics/PorterDuffColorFilter.java
new file mode 100644
index 0000000..01d5825
--- /dev/null
+++ b/android/graphics/PorterDuffColorFilter.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+
+/**
+ * A color filter that can be used to tint the source pixels using a single
+ * color and a specific {@link PorterDuff Porter-Duff composite mode}.
+ */
+public class PorterDuffColorFilter extends ColorFilter {
+ @ColorInt
+ private int mColor;
+ private PorterDuff.Mode mMode;
+
+ /**
+ * Create a color filter that uses the specified color and Porter-Duff mode.
+ *
+ * @param color The ARGB source color used with the specified Porter-Duff mode
+ * @param mode The porter-duff mode that is applied
+ *
+ * @see Color
+ * @see #setColor(int)
+ * @see #setMode(android.graphics.PorterDuff.Mode)
+ */
+ public PorterDuffColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode) {
+ mColor = color;
+ mMode = mode;
+ }
+
+ /**
+ * Returns the ARGB color used to tint the source pixels when this filter
+ * is applied.
+ *
+ * @see Color
+ * @see #setColor(int)
+ *
+ * @hide
+ */
+ @ColorInt
+ public int getColor() {
+ return mColor;
+ }
+
+ /**
+ * Specifies the color to tint the source pixels with when this color
+ * filter is applied.
+ *
+ * @param color An ARGB {@link Color color}
+ *
+ * @see Color
+ * @see #getColor()
+ * @see #getMode()
+ *
+ * @hide
+ */
+ public void setColor(@ColorInt int color) {
+ if (mColor != color) {
+ mColor = color;
+ discardNativeInstance();
+ }
+ }
+
+ /**
+ * Returns the Porter-Duff mode used to composite this color filter's
+ * color with the source pixel when this filter is applied.
+ *
+ * @see PorterDuff
+ * @see #setMode(android.graphics.PorterDuff.Mode)
+ *
+ * @hide
+ */
+ public PorterDuff.Mode getMode() {
+ return mMode;
+ }
+
+ /**
+ * Specifies the Porter-Duff mode to use when compositing this color
+ * filter's color with the source pixel at draw time.
+ *
+ * @see PorterDuff
+ * @see #getMode()
+ * @see #getColor()
+ *
+ * @hide
+ */
+ public void setMode(@NonNull PorterDuff.Mode mode) {
+ if (mode == null) {
+ throw new IllegalArgumentException("mode must be non-null");
+ }
+ mMode = mode;
+ discardNativeInstance();
+ }
+
+ @Override
+ long createNativeInstance() {
+ return native_CreatePorterDuffFilter(mColor, mMode.nativeInt);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ final PorterDuffColorFilter other = (PorterDuffColorFilter) object;
+ return (mColor == other.mColor && mMode.nativeInt == other.mMode.nativeInt);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * mMode.hashCode() + mColor;
+ }
+
+ private static native long native_CreatePorterDuffFilter(int srcColor, int porterDuffMode);
+}
diff --git a/android/graphics/PorterDuffColorFilter_Delegate.java b/android/graphics/PorterDuffColorFilter_Delegate.java
new file mode 100644
index 0000000..ff3f19f
--- /dev/null
+++ b/android/graphics/PorterDuffColorFilter_Delegate.java
@@ -0,0 +1,124 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.graphics.PorterDuff.Mode;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+
+import static com.android.layoutlib.bridge.impl.PorterDuffUtility.getComposite;
+import static com.android.layoutlib.bridge.impl.PorterDuffUtility.getPorterDuffMode;
+
+/**
+ * Delegate implementing the native methods of android.graphics.PorterDuffColorFilter
+ *
+ * Through the layoutlib_create tool, the original native methods of PorterDuffColorFilter have
+ * been replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original PorterDuffColorFilter class.
+ *
+ * Because this extends {@link ColorFilter_Delegate}, there's no need to use a
+ * {@link DelegateManager}, as all the Shader classes will be added to the manager
+ * owned by {@link ColorFilter_Delegate}.
+ *
+ * @see ColorFilter_Delegate
+ *
+ */
+public class PorterDuffColorFilter_Delegate extends ColorFilter_Delegate {
+
+ // ---- delegate data ----
+
+ private final java.awt.Color mSrcColor;
+ private final Mode mMode;
+
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public boolean isSupported() {
+ return true;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ return "PorterDuff Color Filter is not supported for mode: " + mMode.name() + ".";
+ }
+
+ @Override
+ public void applyFilter(Graphics2D g, int width, int height) {
+ g.setComposite(getComposite(mMode, 0xFF));
+ g.setColor(mSrcColor);
+ g.fillRect(0, 0, width, height);
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long native_CreatePorterDuffFilter(int srcColor, int porterDuffMode) {
+ PorterDuffColorFilter_Delegate newDelegate =
+ new PorterDuffColorFilter_Delegate(srcColor, porterDuffMode);
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+
+ // ---- Private delegate/helper methods ----
+
+ private PorterDuffColorFilter_Delegate(int srcColor, int mode) {
+ mSrcColor = new java.awt.Color(srcColor, true /* hasAlpha */);
+ mMode = getCompatibleMode(getPorterDuffMode(mode));
+ }
+
+ // For filtering the colors, the src image should contain the "color" only for pixel values
+ // which are not transparent in the target image. But, we are using a simple rectangular image
+ // completely filled with color. Hence some Composite rules do not apply as intended. However,
+ // in such cases, they can usually be mapped to some other mode, which produces an approximately
+ // equivalent result.
+ private Mode getCompatibleMode(Mode mode) {
+ Mode m = mode;
+ // Modes that are directly supported:
+ // CLEAR, DST, SRC_IN, DST_IN, DST_OUT, SRC_ATOP, DARKEN, LIGHTEN, MULTIPLY, SCREEN,
+ // ADD, OVERLAY
+ switch (mode) {
+ // Modes that can be mapped to one of the supported modes.
+ case SRC:
+ m = Mode.SRC_IN;
+ break;
+ case SRC_OVER:
+ m = Mode.SRC_ATOP;
+ break;
+ case DST_OVER:
+ m = Mode.DST;
+ break;
+ case SRC_OUT:
+ m = Mode.CLEAR;
+ break;
+ case DST_ATOP:
+ m = Mode.DST_IN;
+ break;
+ case XOR:
+ m = Mode.DST_OUT;
+ break;
+ }
+ return m;
+ }
+}
diff --git a/android/graphics/PorterDuffXfermode.java b/android/graphics/PorterDuffXfermode.java
new file mode 100644
index 0000000..84d953d
--- /dev/null
+++ b/android/graphics/PorterDuffXfermode.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * <p>Specialized implementation of {@link Paint}'s
+ * {@link Paint#setXfermode(Xfermode) transfer mode}. Refer to the
+ * documentation of the {@link PorterDuff.Mode} enum for more
+ * information on the available alpha compositing and blending modes.</p>
+ */
+public class PorterDuffXfermode extends Xfermode {
+ /**
+ * Create an xfermode that uses the specified porter-duff mode.
+ *
+ * @param mode The porter-duff mode that is applied
+ */
+ public PorterDuffXfermode(PorterDuff.Mode mode) {
+ porterDuffMode = mode.nativeInt;
+ }
+}
diff --git a/android/graphics/RadialGradient.java b/android/graphics/RadialGradient.java
new file mode 100644
index 0000000..f4b1191
--- /dev/null
+++ b/android/graphics/RadialGradient.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.ColorInt;
+
+public class RadialGradient extends Shader {
+
+ private static final int TYPE_COLORS_AND_POSITIONS = 1;
+ private static final int TYPE_COLOR_CENTER_AND_COLOR_EDGE = 2;
+
+ /**
+ * Type of the RadialGradient: can be either TYPE_COLORS_AND_POSITIONS or
+ * TYPE_COLOR_CENTER_AND_COLOR_EDGE.
+ */
+ private int mType;
+
+ private float mX;
+ private float mY;
+ private float mRadius;
+ private int[] mColors;
+ private float[] mPositions;
+ private int mCenterColor;
+ private int mEdgeColor;
+
+ private TileMode mTileMode;
+
+ /**
+ * Create a shader that draws a radial gradient given the center and radius.
+ *
+ * @param centerX The x-coordinate of the center of the radius
+ * @param centerY The y-coordinate of the center of the radius
+ * @param radius Must be positive. The radius of the circle for this gradient.
+ * @param colors The colors to be distributed between the center and edge of the circle
+ * @param stops May be <code>null</code>. Valid values are between <code>0.0f</code> and
+ * <code>1.0f</code>. The relative position of each corresponding color in
+ * the colors array. If <code>null</code>, colors are distributed evenly
+ * between the center and edge of the circle.
+ * @param tileMode The Shader tiling mode
+ */
+ public RadialGradient(float centerX, float centerY, float radius,
+ @NonNull @ColorInt int colors[], @Nullable float stops[],
+ @NonNull TileMode tileMode) {
+ if (radius <= 0) {
+ throw new IllegalArgumentException("radius must be > 0");
+ }
+ if (colors.length < 2) {
+ throw new IllegalArgumentException("needs >= 2 number of colors");
+ }
+ if (stops != null && colors.length != stops.length) {
+ throw new IllegalArgumentException("color and position arrays must be of equal length");
+ }
+ mType = TYPE_COLORS_AND_POSITIONS;
+ mX = centerX;
+ mY = centerY;
+ mRadius = radius;
+ mColors = colors.clone();
+ mPositions = stops != null ? stops.clone() : null;
+ mTileMode = tileMode;
+ }
+
+ /**
+ * Create a shader that draws a radial gradient given the center and radius.
+ *
+ * @param centerX The x-coordinate of the center of the radius
+ * @param centerY The y-coordinate of the center of the radius
+ * @param radius Must be positive. The radius of the circle for this gradient
+ * @param centerColor The color at the center of the circle.
+ * @param edgeColor The color at the edge of the circle.
+ * @param tileMode The Shader tiling mode
+ */
+ public RadialGradient(float centerX, float centerY, float radius,
+ @ColorInt int centerColor, @ColorInt int edgeColor, @NonNull TileMode tileMode) {
+ if (radius <= 0) {
+ throw new IllegalArgumentException("radius must be > 0");
+ }
+ mType = TYPE_COLOR_CENTER_AND_COLOR_EDGE;
+ mX = centerX;
+ mY = centerY;
+ mRadius = radius;
+ mCenterColor = centerColor;
+ mEdgeColor = edgeColor;
+ mTileMode = tileMode;
+ }
+
+ @Override
+ long createNativeInstance(long nativeMatrix) {
+ if (mType == TYPE_COLORS_AND_POSITIONS) {
+ return nativeCreate1(nativeMatrix, mX, mY, mRadius,
+ mColors, mPositions, mTileMode.nativeInt);
+ } else { // TYPE_COLOR_CENTER_AND_COLOR_EDGE
+ return nativeCreate2(nativeMatrix, mX, mY, mRadius,
+ mCenterColor, mEdgeColor, mTileMode.nativeInt);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected Shader copy() {
+ final RadialGradient copy;
+ if (mType == TYPE_COLORS_AND_POSITIONS) {
+ copy = new RadialGradient(mX, mY, mRadius, mColors.clone(),
+ mPositions != null ? mPositions.clone() : null, mTileMode);
+ } else { // TYPE_COLOR_CENTER_AND_COLOR_EDGE
+ copy = new RadialGradient(mX, mY, mRadius, mCenterColor, mEdgeColor, mTileMode);
+ }
+ copyLocalMatrix(copy);
+ return copy;
+ }
+
+ private static native long nativeCreate1(long matrix, float x, float y, float radius,
+ int colors[], float positions[], int tileMode);
+ private static native long nativeCreate2(long matrix, float x, float y, float radius,
+ int color0, int color1, int tileMode);
+}
+
diff --git a/android/graphics/RadialGradient_Delegate.java b/android/graphics/RadialGradient_Delegate.java
new file mode 100644
index 0000000..1defc90
--- /dev/null
+++ b/android/graphics/RadialGradient_Delegate.java
@@ -0,0 +1,204 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.graphics.Shader.TileMode;
+
+import java.awt.image.ColorModel;
+import java.awt.image.DataBufferInt;
+import java.awt.image.Raster;
+import java.awt.image.SampleModel;
+
+/**
+ * Delegate implementing the native methods of android.graphics.RadialGradient
+ *
+ * Through the layoutlib_create tool, the original native methods of RadialGradient have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original RadialGradient class.
+ *
+ * Because this extends {@link Shader_Delegate}, there's no need to use a {@link DelegateManager},
+ * as all the Shader classes will be added to the manager owned by {@link Shader_Delegate}.
+ *
+ * @see Shader_Delegate
+ *
+ */
+public class RadialGradient_Delegate extends Gradient_Delegate {
+
+ // ---- delegate data ----
+ private java.awt.Paint mJavaPaint;
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public java.awt.Paint getJavaPaint() {
+ return mJavaPaint;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate1(long matrix, float x, float y, float radius,
+ int colors[], float positions[], int tileMode) {
+ RadialGradient_Delegate newDelegate = new RadialGradient_Delegate(matrix, x, y, radius,
+ colors, positions, Shader_Delegate.getTileMode(tileMode));
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate2(long matrix, float x, float y, float radius,
+ int color0, int color1, int tileMode) {
+ return nativeCreate1(matrix, x, y, radius, new int[] { color0, color1 },
+ null /*positions*/, tileMode);
+ }
+
+ // ---- Private delegate/helper methods ----
+
+ /**
+ * Create a shader that draws a radial gradient given the center and radius.
+ *
+ * @param nativeMatrix reference to the shader's native transformation matrix
+ * @param x The x-coordinate of the center of the radius
+ * @param y The y-coordinate of the center of the radius
+ * @param radius Must be positive. The radius of the circle for this
+ * gradient
+ * @param colors The colors to be distributed between the center and edge of
+ * the circle
+ * @param positions May be NULL. The relative position of each corresponding
+ * color in the colors array. If this is NULL, the the colors are
+ * distributed evenly between the center and edge of the circle.
+ * @param tile The Shader tiling mode
+ */
+ private RadialGradient_Delegate(long nativeMatrix, float x, float y, float radius,
+ int colors[], float positions[], TileMode tile) {
+ super(nativeMatrix, colors, positions);
+ mJavaPaint = new RadialGradientPaint(x, y, radius, mColors, mPositions, tile);
+ }
+
+ private class RadialGradientPaint extends GradientPaint {
+
+ private final float mX;
+ private final float mY;
+ private final float mRadius;
+
+ public RadialGradientPaint(float x, float y, float radius,
+ int[] colors, float[] positions, TileMode mode) {
+ super(colors, positions, mode);
+ mX = x;
+ mY = y;
+ mRadius = radius;
+ }
+
+ @Override
+ public java.awt.PaintContext createContext(
+ java.awt.image.ColorModel colorModel,
+ java.awt.Rectangle deviceBounds,
+ java.awt.geom.Rectangle2D userBounds,
+ java.awt.geom.AffineTransform xform,
+ java.awt.RenderingHints hints) {
+ precomputeGradientColors();
+
+ java.awt.geom.AffineTransform canvasMatrix;
+ try {
+ canvasMatrix = xform.createInverse();
+ } catch (java.awt.geom.NoninvertibleTransformException e) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE,
+ "Unable to inverse matrix in RadialGradient", e, null /*data*/);
+ canvasMatrix = new java.awt.geom.AffineTransform();
+ }
+
+ java.awt.geom.AffineTransform localMatrix = getLocalMatrix();
+ try {
+ localMatrix = localMatrix.createInverse();
+ } catch (java.awt.geom.NoninvertibleTransformException e) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE,
+ "Unable to inverse matrix in RadialGradient", e, null /*data*/);
+ localMatrix = new java.awt.geom.AffineTransform();
+ }
+
+ return new RadialGradientPaintContext(canvasMatrix, localMatrix, colorModel);
+ }
+
+ private class RadialGradientPaintContext implements java.awt.PaintContext {
+
+ private final java.awt.geom.AffineTransform mCanvasMatrix;
+ private final java.awt.geom.AffineTransform mLocalMatrix;
+ private final java.awt.image.ColorModel mColorModel;
+
+ public RadialGradientPaintContext(
+ java.awt.geom.AffineTransform canvasMatrix,
+ java.awt.geom.AffineTransform localMatrix,
+ java.awt.image.ColorModel colorModel) {
+ mCanvasMatrix = canvasMatrix;
+ mLocalMatrix = localMatrix;
+ mColorModel = colorModel.hasAlpha() ? colorModel : ColorModel.getRGBdefault();
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public java.awt.image.ColorModel getColorModel() {
+ return mColorModel;
+ }
+
+ @Override
+ public java.awt.image.Raster getRaster(int x, int y, int w, int h) {
+ int[] data = new int[w*h];
+
+ // compute distance from each point to the center, and figure out the distance from
+ // it.
+ int index = 0;
+ float[] pt1 = new float[2];
+ float[] pt2 = new float[2];
+ for (int iy = 0 ; iy < h ; iy++) {
+ for (int ix = 0 ; ix < w ; ix++) {
+ // handle the canvas transform
+ pt1[0] = x + ix;
+ pt1[1] = y + iy;
+ mCanvasMatrix.transform(pt1, 0, pt2, 0, 1);
+
+ // handle the local matrix
+ pt1[0] = pt2[0] - mX;
+ pt1[1] = pt2[1] - mY;
+ mLocalMatrix.transform(pt1, 0, pt2, 0, 1);
+
+ float _x = pt2[0];
+ float _y = pt2[1];
+ float distance = (float) Math.hypot(_x, _y);
+
+ data[index++] = getGradientColor(distance / mRadius);
+ }
+ }
+
+ DataBufferInt dataBuffer = new DataBufferInt(data, data.length);
+ SampleModel colorModel = mColorModel.createCompatibleSampleModel(w, h);
+ return Raster.createWritableRaster(colorModel, dataBuffer, null);
+ }
+
+ }
+ }
+
+}
diff --git a/android/graphics/Rasterizer.java b/android/graphics/Rasterizer.java
new file mode 100644
index 0000000..29d82fa
--- /dev/null
+++ b/android/graphics/Rasterizer.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 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.
+ */
+
+// This file was generated from the C++ include file: SkRasterizer.h
+// Any changes made to this file will be discarded by the build.
+// To change this file, either edit the include, or device/tools/gluemaker/main.cpp,
+// or one of the auxilary file specifications in device/tools/gluemaker.
+
+package android.graphics;
+
+/**
+ * @removed feature is not supported by hw-accerlerated or PDF backends
+ */
+public class Rasterizer {
+
+ protected void finalize() throws Throwable { }
+}
diff --git a/android/graphics/Rect.java b/android/graphics/Rect.java
new file mode 100644
index 0000000..3dc928d
--- /dev/null
+++ b/android/graphics/Rect.java
@@ -0,0 +1,673 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.annotation.CheckResult;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import android.text.TextUtils;
+import android.util.proto.ProtoOutputStream;
+import java.io.PrintWriter;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Rect holds four integer coordinates for a rectangle. The rectangle is
+ * represented by the coordinates of its 4 edges (left, top, right bottom).
+ * These fields can be accessed directly. Use width() and height() to retrieve
+ * the rectangle's width and height. Note: most methods do not check to see that
+ * the coordinates are sorted correctly (i.e. left <= right and top <= bottom).
+ * <p>
+ * Note that the right and bottom coordinates are exclusive. This means a Rect
+ * being drawn untransformed onto a {@link android.graphics.Canvas} will draw
+ * into the column and row described by its left and top coordinates, but not
+ * those of its bottom and right.
+ */
+public final class Rect implements Parcelable {
+ public int left;
+ public int top;
+ public int right;
+ public int bottom;
+
+ /**
+ * A helper class for flattened rectange pattern recognition. A separate
+ * class to avoid an initialization dependency on a regular expression
+ * causing Rect to not be initializable with an ahead-of-time compilation
+ * scheme.
+ */
+ private static final class UnflattenHelper {
+ private static final Pattern FLATTENED_PATTERN = Pattern.compile(
+ "(-?\\d+) (-?\\d+) (-?\\d+) (-?\\d+)");
+
+ static Matcher getMatcher(String str) {
+ return FLATTENED_PATTERN.matcher(str);
+ }
+ }
+
+ /**
+ * Create a new empty Rect. All coordinates are initialized to 0.
+ */
+ public Rect() {}
+
+ /**
+ * Create a new rectangle with the specified coordinates. Note: no range
+ * checking is performed, so the caller must ensure that left <= right and
+ * top <= bottom.
+ *
+ * @param left The X coordinate of the left side of the rectangle
+ * @param top The Y coordinate of the top of the rectangle
+ * @param right The X coordinate of the right side of the rectangle
+ * @param bottom The Y coordinate of the bottom of the rectangle
+ */
+ public Rect(int left, int top, int right, int bottom) {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+
+ /**
+ * Create a new rectangle, initialized with the values in the specified
+ * rectangle (which is left unmodified).
+ *
+ * @param r The rectangle whose coordinates are copied into the new
+ * rectangle.
+ */
+ public Rect(Rect r) {
+ if (r == null) {
+ left = top = right = bottom = 0;
+ } else {
+ left = r.left;
+ top = r.top;
+ right = r.right;
+ bottom = r.bottom;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Rect r = (Rect) o;
+ return left == r.left && top == r.top && right == r.right && bottom == r.bottom;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = left;
+ result = 31 * result + top;
+ result = 31 * result + right;
+ result = 31 * result + bottom;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(32);
+ sb.append("Rect("); sb.append(left); sb.append(", ");
+ sb.append(top); sb.append(" - "); sb.append(right);
+ sb.append(", "); sb.append(bottom); sb.append(")");
+ return sb.toString();
+ }
+
+ /**
+ * Return a string representation of the rectangle in a compact form.
+ */
+ public String toShortString() {
+ return toShortString(new StringBuilder(32));
+ }
+
+ /**
+ * Return a string representation of the rectangle in a compact form.
+ * @hide
+ */
+ public String toShortString(StringBuilder sb) {
+ sb.setLength(0);
+ sb.append('['); sb.append(left); sb.append(',');
+ sb.append(top); sb.append("]["); sb.append(right);
+ sb.append(','); sb.append(bottom); sb.append(']');
+ return sb.toString();
+ }
+
+ /**
+ * Return a string representation of the rectangle in a well-defined format.
+ *
+ * <p>You can later recover the Rect from this string through
+ * {@link #unflattenFromString(String)}.
+ *
+ * @return Returns a new String of the form "left top right bottom"
+ */
+ public String flattenToString() {
+ StringBuilder sb = new StringBuilder(32);
+ // WARNING: Do not change the format of this string, it must be
+ // preserved because Rects are saved in this flattened format.
+ sb.append(left);
+ sb.append(' ');
+ sb.append(top);
+ sb.append(' ');
+ sb.append(right);
+ sb.append(' ');
+ sb.append(bottom);
+ return sb.toString();
+ }
+
+ /**
+ * Returns a Rect from a string of the form returned by {@link #flattenToString},
+ * or null if the string is not of that form.
+ */
+ public static Rect unflattenFromString(String str) {
+ if (TextUtils.isEmpty(str)) {
+ return null;
+ }
+
+ Matcher matcher = UnflattenHelper.getMatcher(str);
+ if (!matcher.matches()) {
+ return null;
+ }
+ return new Rect(Integer.parseInt(matcher.group(1)),
+ Integer.parseInt(matcher.group(2)),
+ Integer.parseInt(matcher.group(3)),
+ Integer.parseInt(matcher.group(4)));
+ }
+
+ /**
+ * Print short representation to given writer.
+ * @hide
+ */
+ public void printShortString(PrintWriter pw) {
+ pw.print('['); pw.print(left); pw.print(',');
+ pw.print(top); pw.print("]["); pw.print(right);
+ pw.print(','); pw.print(bottom); pw.print(']');
+ }
+
+ /**
+ * Write to a protocol buffer output stream.
+ * Protocol buffer message definition at {@link android.graphics.RectProto}
+ *
+ * @param protoOutputStream Stream to write the Rect object to.
+ * @param fieldId Field Id of the Rect as defined in the parent message
+ * @hide
+ */
+ public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) {
+ final long token = protoOutputStream.start(fieldId);
+ protoOutputStream.write(RectProto.LEFT, left);
+ protoOutputStream.write(RectProto.TOP, top);
+ protoOutputStream.write(RectProto.RIGHT, right);
+ protoOutputStream.write(RectProto.BOTTOM, bottom);
+ protoOutputStream.end(token);
+ }
+
+ /**
+ * Returns true if the rectangle is empty (left >= right or top >= bottom)
+ */
+ public final boolean isEmpty() {
+ return left >= right || top >= bottom;
+ }
+
+ /**
+ * @return the rectangle's width. This does not check for a valid rectangle
+ * (i.e. left <= right) so the result may be negative.
+ */
+ public final int width() {
+ return right - left;
+ }
+
+ /**
+ * @return the rectangle's height. This does not check for a valid rectangle
+ * (i.e. top <= bottom) so the result may be negative.
+ */
+ public final int height() {
+ return bottom - top;
+ }
+
+ /**
+ * @return the horizontal center of the rectangle. If the computed value
+ * is fractional, this method returns the largest integer that is
+ * less than the computed value.
+ */
+ public final int centerX() {
+ return (left + right) >> 1;
+ }
+
+ /**
+ * @return the vertical center of the rectangle. If the computed value
+ * is fractional, this method returns the largest integer that is
+ * less than the computed value.
+ */
+ public final int centerY() {
+ return (top + bottom) >> 1;
+ }
+
+ /**
+ * @return the exact horizontal center of the rectangle as a float.
+ */
+ public final float exactCenterX() {
+ return (left + right) * 0.5f;
+ }
+
+ /**
+ * @return the exact vertical center of the rectangle as a float.
+ */
+ public final float exactCenterY() {
+ return (top + bottom) * 0.5f;
+ }
+
+ /**
+ * Set the rectangle to (0,0,0,0)
+ */
+ public void setEmpty() {
+ left = right = top = bottom = 0;
+ }
+
+ /**
+ * Set the rectangle's coordinates to the specified values. Note: no range
+ * checking is performed, so it is up to the caller to ensure that
+ * left <= right and top <= bottom.
+ *
+ * @param left The X coordinate of the left side of the rectangle
+ * @param top The Y coordinate of the top of the rectangle
+ * @param right The X coordinate of the right side of the rectangle
+ * @param bottom The Y coordinate of the bottom of the rectangle
+ */
+ public void set(int left, int top, int right, int bottom) {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+
+ /**
+ * Copy the coordinates from src into this rectangle.
+ *
+ * @param src The rectangle whose coordinates are copied into this
+ * rectangle.
+ */
+ public void set(Rect src) {
+ this.left = src.left;
+ this.top = src.top;
+ this.right = src.right;
+ this.bottom = src.bottom;
+ }
+
+ /**
+ * Offset the rectangle by adding dx to its left and right coordinates, and
+ * adding dy to its top and bottom coordinates.
+ *
+ * @param dx The amount to add to the rectangle's left and right coordinates
+ * @param dy The amount to add to the rectangle's top and bottom coordinates
+ */
+ public void offset(int dx, int dy) {
+ left += dx;
+ top += dy;
+ right += dx;
+ bottom += dy;
+ }
+
+ /**
+ * Offset the rectangle to a specific (left, top) position,
+ * keeping its width and height the same.
+ *
+ * @param newLeft The new "left" coordinate for the rectangle
+ * @param newTop The new "top" coordinate for the rectangle
+ */
+ public void offsetTo(int newLeft, int newTop) {
+ right += newLeft - left;
+ bottom += newTop - top;
+ left = newLeft;
+ top = newTop;
+ }
+
+ /**
+ * Inset the rectangle by (dx,dy). If dx is positive, then the sides are
+ * moved inwards, making the rectangle narrower. If dx is negative, then the
+ * sides are moved outwards, making the rectangle wider. The same holds true
+ * for dy and the top and bottom.
+ *
+ * @param dx The amount to add(subtract) from the rectangle's left(right)
+ * @param dy The amount to add(subtract) from the rectangle's top(bottom)
+ */
+ public void inset(int dx, int dy) {
+ left += dx;
+ top += dy;
+ right -= dx;
+ bottom -= dy;
+ }
+
+ /**
+ * Insets the rectangle on all sides specified by the dimensions of the {@code insets}
+ * rectangle.
+ * @hide
+ * @param insets The rectangle specifying the insets on all side.
+ */
+ public void inset(Rect insets) {
+ left += insets.left;
+ top += insets.top;
+ right -= insets.right;
+ bottom -= insets.bottom;
+ }
+
+ /**
+ * Insets the rectangle on all sides specified by the insets.
+ * @hide
+ * @param left The amount to add from the rectangle's left
+ * @param top The amount to add from the rectangle's top
+ * @param right The amount to subtract from the rectangle's right
+ * @param bottom The amount to subtract from the rectangle's bottom
+ */
+ public void inset(int left, int top, int right, int bottom) {
+ this.left += left;
+ this.top += top;
+ this.right -= right;
+ this.bottom -= bottom;
+ }
+
+ /**
+ * Returns true if (x,y) is inside the rectangle. The left and top are
+ * considered to be inside, while the right and bottom are not. This means
+ * that for a x,y to be contained: left <= x < right and top <= y < bottom.
+ * An empty rectangle never contains any point.
+ *
+ * @param x The X coordinate of the point being tested for containment
+ * @param y The Y coordinate of the point being tested for containment
+ * @return true iff (x,y) are contained by the rectangle, where containment
+ * means left <= x < right and top <= y < bottom
+ */
+ public boolean contains(int x, int y) {
+ return left < right && top < bottom // check for empty first
+ && x >= left && x < right && y >= top && y < bottom;
+ }
+
+ /**
+ * Returns true iff the 4 specified sides of a rectangle are inside or equal
+ * to this rectangle. i.e. is this rectangle a superset of the specified
+ * rectangle. An empty rectangle never contains another rectangle.
+ *
+ * @param left The left side of the rectangle being tested for containment
+ * @param top The top of the rectangle being tested for containment
+ * @param right The right side of the rectangle being tested for containment
+ * @param bottom The bottom of the rectangle being tested for containment
+ * @return true iff the the 4 specified sides of a rectangle are inside or
+ * equal to this rectangle
+ */
+ public boolean contains(int left, int top, int right, int bottom) {
+ // check for empty first
+ return this.left < this.right && this.top < this.bottom
+ // now check for containment
+ && this.left <= left && this.top <= top
+ && this.right >= right && this.bottom >= bottom;
+ }
+
+ /**
+ * Returns true iff the specified rectangle r is inside or equal to this
+ * rectangle. An empty rectangle never contains another rectangle.
+ *
+ * @param r The rectangle being tested for containment.
+ * @return true iff the specified rectangle r is inside or equal to this
+ * rectangle
+ */
+ public boolean contains(Rect r) {
+ // check for empty first
+ return this.left < this.right && this.top < this.bottom
+ // now check for containment
+ && left <= r.left && top <= r.top && right >= r.right && bottom >= r.bottom;
+ }
+
+ /**
+ * If the rectangle specified by left,top,right,bottom intersects this
+ * rectangle, return true and set this rectangle to that intersection,
+ * otherwise return false and do not change this rectangle. No check is
+ * performed to see if either rectangle is empty. Note: To just test for
+ * intersection, use {@link #intersects(Rect, Rect)}.
+ *
+ * @param left The left side of the rectangle being intersected with this
+ * rectangle
+ * @param top The top of the rectangle being intersected with this rectangle
+ * @param right The right side of the rectangle being intersected with this
+ * rectangle.
+ * @param bottom The bottom of the rectangle being intersected with this
+ * rectangle.
+ * @return true if the specified rectangle and this rectangle intersect
+ * (and this rectangle is then set to that intersection) else
+ * return false and do not change this rectangle.
+ */
+ @CheckResult
+ public boolean intersect(int left, int top, int right, int bottom) {
+ if (this.left < right && left < this.right && this.top < bottom && top < this.bottom) {
+ if (this.left < left) this.left = left;
+ if (this.top < top) this.top = top;
+ if (this.right > right) this.right = right;
+ if (this.bottom > bottom) this.bottom = bottom;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * If the specified rectangle intersects this rectangle, return true and set
+ * this rectangle to that intersection, otherwise return false and do not
+ * change this rectangle. No check is performed to see if either rectangle
+ * is empty. To just test for intersection, use intersects()
+ *
+ * @param r The rectangle being intersected with this rectangle.
+ * @return true if the specified rectangle and this rectangle intersect
+ * (and this rectangle is then set to that intersection) else
+ * return false and do not change this rectangle.
+ */
+ @CheckResult
+ public boolean intersect(Rect r) {
+ return intersect(r.left, r.top, r.right, r.bottom);
+ }
+
+ /**
+ * If rectangles a and b intersect, return true and set this rectangle to
+ * that intersection, otherwise return false and do not change this
+ * rectangle. No check is performed to see if either rectangle is empty.
+ * To just test for intersection, use intersects()
+ *
+ * @param a The first rectangle being intersected with
+ * @param b The second rectangle being intersected with
+ * @return true iff the two specified rectangles intersect. If they do, set
+ * this rectangle to that intersection. If they do not, return
+ * false and do not change this rectangle.
+ */
+ @CheckResult
+ public boolean setIntersect(Rect a, Rect b) {
+ if (a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom) {
+ left = Math.max(a.left, b.left);
+ top = Math.max(a.top, b.top);
+ right = Math.min(a.right, b.right);
+ bottom = Math.min(a.bottom, b.bottom);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this rectangle intersects the specified rectangle.
+ * In no event is this rectangle modified. No check is performed to see
+ * if either rectangle is empty. To record the intersection, use intersect()
+ * or setIntersect().
+ *
+ * @param left The left side of the rectangle being tested for intersection
+ * @param top The top of the rectangle being tested for intersection
+ * @param right The right side of the rectangle being tested for
+ * intersection
+ * @param bottom The bottom of the rectangle being tested for intersection
+ * @return true iff the specified rectangle intersects this rectangle. In
+ * no event is this rectangle modified.
+ */
+ public boolean intersects(int left, int top, int right, int bottom) {
+ return this.left < right && left < this.right && this.top < bottom && top < this.bottom;
+ }
+
+ /**
+ * Returns true iff the two specified rectangles intersect. In no event are
+ * either of the rectangles modified. To record the intersection,
+ * use {@link #intersect(Rect)} or {@link #setIntersect(Rect, Rect)}.
+ *
+ * @param a The first rectangle being tested for intersection
+ * @param b The second rectangle being tested for intersection
+ * @return true iff the two specified rectangles intersect. In no event are
+ * either of the rectangles modified.
+ */
+ public static boolean intersects(Rect a, Rect b) {
+ return a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
+ }
+
+ /**
+ * Update this Rect to enclose itself and the specified rectangle. If the
+ * specified rectangle is empty, nothing is done. If this rectangle is empty
+ * it is set to the specified rectangle.
+ *
+ * @param left The left edge being unioned with this rectangle
+ * @param top The top edge being unioned with this rectangle
+ * @param right The right edge being unioned with this rectangle
+ * @param bottom The bottom edge being unioned with this rectangle
+ */
+ public void union(int left, int top, int right, int bottom) {
+ if ((left < right) && (top < bottom)) {
+ if ((this.left < this.right) && (this.top < this.bottom)) {
+ if (this.left > left) this.left = left;
+ if (this.top > top) this.top = top;
+ if (this.right < right) this.right = right;
+ if (this.bottom < bottom) this.bottom = bottom;
+ } else {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+ }
+ }
+
+ /**
+ * Update this Rect to enclose itself and the specified rectangle. If the
+ * specified rectangle is empty, nothing is done. If this rectangle is empty
+ * it is set to the specified rectangle.
+ *
+ * @param r The rectangle being unioned with this rectangle
+ */
+ public void union(Rect r) {
+ union(r.left, r.top, r.right, r.bottom);
+ }
+
+ /**
+ * Update this Rect to enclose itself and the [x,y] coordinate. There is no
+ * check to see that this rectangle is non-empty.
+ *
+ * @param x The x coordinate of the point to add to the rectangle
+ * @param y The y coordinate of the point to add to the rectangle
+ */
+ public void union(int x, int y) {
+ if (x < left) {
+ left = x;
+ } else if (x > right) {
+ right = x;
+ }
+ if (y < top) {
+ top = y;
+ } else if (y > bottom) {
+ bottom = y;
+ }
+ }
+
+ /**
+ * Swap top/bottom or left/right if there are flipped (i.e. left > right
+ * and/or top > bottom). This can be called if
+ * the edges are computed separately, and may have crossed over each other.
+ * If the edges are already correct (i.e. left <= right and top <= bottom)
+ * then nothing is done.
+ */
+ public void sort() {
+ if (left > right) {
+ int temp = left;
+ left = right;
+ right = temp;
+ }
+ if (top > bottom) {
+ int temp = top;
+ top = bottom;
+ bottom = temp;
+ }
+ }
+
+ /**
+ * Parcelable interface methods
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Write this rectangle to the specified parcel. To restore a rectangle from
+ * a parcel, use readFromParcel()
+ * @param out The parcel to write the rectangle's coordinates into
+ */
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(left);
+ out.writeInt(top);
+ out.writeInt(right);
+ out.writeInt(bottom);
+ }
+
+ public static final Parcelable.Creator<Rect> CREATOR = new Parcelable.Creator<Rect>() {
+ /**
+ * Return a new rectangle from the data in the specified parcel.
+ */
+ public Rect createFromParcel(Parcel in) {
+ Rect r = new Rect();
+ r.readFromParcel(in);
+ return r;
+ }
+
+ /**
+ * Return an array of rectangles of the specified size.
+ */
+ public Rect[] newArray(int size) {
+ return new Rect[size];
+ }
+ };
+
+ /**
+ * Set the rectangle's coordinates from the data stored in the specified
+ * parcel. To write a rectangle to a parcel, call writeToParcel().
+ *
+ * @param in The parcel to read the rectangle's coordinates from
+ */
+ public void readFromParcel(Parcel in) {
+ left = in.readInt();
+ top = in.readInt();
+ right = in.readInt();
+ bottom = in.readInt();
+ }
+
+ /**
+ * Scales up the rect by the given scale.
+ * @hide
+ */
+ public void scale(float scale) {
+ if (scale != 1.0f) {
+ left = (int) (left * scale + 0.5f);
+ top = (int) (top * scale + 0.5f);
+ right = (int) (right * scale + 0.5f);
+ bottom = (int) (bottom * scale + 0.5f);
+ }
+ }
+
+}
diff --git a/android/graphics/RectF.java b/android/graphics/RectF.java
new file mode 100644
index 0000000..b490545
--- /dev/null
+++ b/android/graphics/RectF.java
@@ -0,0 +1,599 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import java.io.PrintWriter;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.android.internal.util.FastMath;
+
+/**
+ * RectF holds four float coordinates for a rectangle. The rectangle is
+ * represented by the coordinates of its 4 edges (left, top, right bottom).
+ * These fields can be accessed directly. Use width() and height() to retrieve
+ * the rectangle's width and height. Note: most methods do not check to see that
+ * the coordinates are sorted correctly (i.e. left <= right and top <= bottom).
+ */
+public class RectF implements Parcelable {
+ public float left;
+ public float top;
+ public float right;
+ public float bottom;
+
+ /**
+ * Create a new empty RectF. All coordinates are initialized to 0.
+ */
+ public RectF() {}
+
+ /**
+ * Create a new rectangle with the specified coordinates. Note: no range
+ * checking is performed, so the caller must ensure that left <= right and
+ * top <= bottom.
+ *
+ * @param left The X coordinate of the left side of the rectangle
+ * @param top The Y coordinate of the top of the rectangle
+ * @param right The X coordinate of the right side of the rectangle
+ * @param bottom The Y coordinate of the bottom of the rectangle
+ */
+ public RectF(float left, float top, float right, float bottom) {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+
+ /**
+ * Create a new rectangle, initialized with the values in the specified
+ * rectangle (which is left unmodified).
+ *
+ * @param r The rectangle whose coordinates are copied into the new
+ * rectangle.
+ */
+ public RectF(RectF r) {
+ if (r == null) {
+ left = top = right = bottom = 0.0f;
+ } else {
+ left = r.left;
+ top = r.top;
+ right = r.right;
+ bottom = r.bottom;
+ }
+ }
+
+ public RectF(Rect r) {
+ if (r == null) {
+ left = top = right = bottom = 0.0f;
+ } else {
+ left = r.left;
+ top = r.top;
+ right = r.right;
+ bottom = r.bottom;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ RectF r = (RectF) o;
+ return left == r.left && top == r.top && right == r.right && bottom == r.bottom;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (left != +0.0f ? Float.floatToIntBits(left) : 0);
+ result = 31 * result + (top != +0.0f ? Float.floatToIntBits(top) : 0);
+ result = 31 * result + (right != +0.0f ? Float.floatToIntBits(right) : 0);
+ result = 31 * result + (bottom != +0.0f ? Float.floatToIntBits(bottom) : 0);
+ return result;
+ }
+
+ public String toString() {
+ return "RectF(" + left + ", " + top + ", "
+ + right + ", " + bottom + ")";
+ }
+
+ /**
+ * Return a string representation of the rectangle in a compact form.
+ */
+ public String toShortString() {
+ return toShortString(new StringBuilder(32));
+ }
+
+ /**
+ * Return a string representation of the rectangle in a compact form.
+ * @hide
+ */
+ public String toShortString(StringBuilder sb) {
+ sb.setLength(0);
+ sb.append('['); sb.append(left); sb.append(',');
+ sb.append(top); sb.append("]["); sb.append(right);
+ sb.append(','); sb.append(bottom); sb.append(']');
+ return sb.toString();
+ }
+
+ /**
+ * Print short representation to given writer.
+ * @hide
+ */
+ public void printShortString(PrintWriter pw) {
+ pw.print('['); pw.print(left); pw.print(',');
+ pw.print(top); pw.print("]["); pw.print(right);
+ pw.print(','); pw.print(bottom); pw.print(']');
+ }
+
+ /**
+ * Returns true if the rectangle is empty (left >= right or top >= bottom)
+ */
+ public final boolean isEmpty() {
+ return left >= right || top >= bottom;
+ }
+
+ /**
+ * @return the rectangle's width. This does not check for a valid rectangle
+ * (i.e. left <= right) so the result may be negative.
+ */
+ public final float width() {
+ return right - left;
+ }
+
+ /**
+ * @return the rectangle's height. This does not check for a valid rectangle
+ * (i.e. top <= bottom) so the result may be negative.
+ */
+ public final float height() {
+ return bottom - top;
+ }
+
+ /**
+ * @return the horizontal center of the rectangle. This does not check for
+ * a valid rectangle (i.e. left <= right)
+ */
+ public final float centerX() {
+ return (left + right) * 0.5f;
+ }
+
+ /**
+ * @return the vertical center of the rectangle. This does not check for
+ * a valid rectangle (i.e. top <= bottom)
+ */
+ public final float centerY() {
+ return (top + bottom) * 0.5f;
+ }
+
+ /**
+ * Set the rectangle to (0,0,0,0)
+ */
+ public void setEmpty() {
+ left = right = top = bottom = 0;
+ }
+
+ /**
+ * Set the rectangle's coordinates to the specified values. Note: no range
+ * checking is performed, so it is up to the caller to ensure that
+ * left <= right and top <= bottom.
+ *
+ * @param left The X coordinate of the left side of the rectangle
+ * @param top The Y coordinate of the top of the rectangle
+ * @param right The X coordinate of the right side of the rectangle
+ * @param bottom The Y coordinate of the bottom of the rectangle
+ */
+ public void set(float left, float top, float right, float bottom) {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+
+ /**
+ * Copy the coordinates from src into this rectangle.
+ *
+ * @param src The rectangle whose coordinates are copied into this
+ * rectangle.
+ */
+ public void set(RectF src) {
+ this.left = src.left;
+ this.top = src.top;
+ this.right = src.right;
+ this.bottom = src.bottom;
+ }
+
+ /**
+ * Copy the coordinates from src into this rectangle.
+ *
+ * @param src The rectangle whose coordinates are copied into this
+ * rectangle.
+ */
+ public void set(Rect src) {
+ this.left = src.left;
+ this.top = src.top;
+ this.right = src.right;
+ this.bottom = src.bottom;
+ }
+
+ /**
+ * Offset the rectangle by adding dx to its left and right coordinates, and
+ * adding dy to its top and bottom coordinates.
+ *
+ * @param dx The amount to add to the rectangle's left and right coordinates
+ * @param dy The amount to add to the rectangle's top and bottom coordinates
+ */
+ public void offset(float dx, float dy) {
+ left += dx;
+ top += dy;
+ right += dx;
+ bottom += dy;
+ }
+
+ /**
+ * Offset the rectangle to a specific (left, top) position,
+ * keeping its width and height the same.
+ *
+ * @param newLeft The new "left" coordinate for the rectangle
+ * @param newTop The new "top" coordinate for the rectangle
+ */
+ public void offsetTo(float newLeft, float newTop) {
+ right += newLeft - left;
+ bottom += newTop - top;
+ left = newLeft;
+ top = newTop;
+ }
+
+ /**
+ * Inset the rectangle by (dx,dy). If dx is positive, then the sides are
+ * moved inwards, making the rectangle narrower. If dx is negative, then the
+ * sides are moved outwards, making the rectangle wider. The same holds true
+ * for dy and the top and bottom.
+ *
+ * @param dx The amount to add(subtract) from the rectangle's left(right)
+ * @param dy The amount to add(subtract) from the rectangle's top(bottom)
+ */
+ public void inset(float dx, float dy) {
+ left += dx;
+ top += dy;
+ right -= dx;
+ bottom -= dy;
+ }
+
+ /**
+ * Returns true if (x,y) is inside the rectangle. The left and top are
+ * considered to be inside, while the right and bottom are not. This means
+ * that for a x,y to be contained: left <= x < right and top <= y < bottom.
+ * An empty rectangle never contains any point.
+ *
+ * @param x The X coordinate of the point being tested for containment
+ * @param y The Y coordinate of the point being tested for containment
+ * @return true iff (x,y) are contained by the rectangle, where containment
+ * means left <= x < right and top <= y < bottom
+ */
+ public boolean contains(float x, float y) {
+ return left < right && top < bottom // check for empty first
+ && x >= left && x < right && y >= top && y < bottom;
+ }
+
+ /**
+ * Returns true iff the 4 specified sides of a rectangle are inside or equal
+ * to this rectangle. i.e. is this rectangle a superset of the specified
+ * rectangle. An empty rectangle never contains another rectangle.
+ *
+ * @param left The left side of the rectangle being tested for containment
+ * @param top The top of the rectangle being tested for containment
+ * @param right The right side of the rectangle being tested for containment
+ * @param bottom The bottom of the rectangle being tested for containment
+ * @return true iff the the 4 specified sides of a rectangle are inside or
+ * equal to this rectangle
+ */
+ public boolean contains(float left, float top, float right, float bottom) {
+ // check for empty first
+ return this.left < this.right && this.top < this.bottom
+ // now check for containment
+ && this.left <= left && this.top <= top
+ && this.right >= right && this.bottom >= bottom;
+ }
+
+ /**
+ * Returns true iff the specified rectangle r is inside or equal to this
+ * rectangle. An empty rectangle never contains another rectangle.
+ *
+ * @param r The rectangle being tested for containment.
+ * @return true iff the specified rectangle r is inside or equal to this
+ * rectangle
+ */
+ public boolean contains(RectF r) {
+ // check for empty first
+ return this.left < this.right && this.top < this.bottom
+ // now check for containment
+ && left <= r.left && top <= r.top
+ && right >= r.right && bottom >= r.bottom;
+ }
+
+ /**
+ * If the rectangle specified by left,top,right,bottom intersects this
+ * rectangle, return true and set this rectangle to that intersection,
+ * otherwise return false and do not change this rectangle. No check is
+ * performed to see if either rectangle is empty. Note: To just test for
+ * intersection, use intersects()
+ *
+ * @param left The left side of the rectangle being intersected with this
+ * rectangle
+ * @param top The top of the rectangle being intersected with this rectangle
+ * @param right The right side of the rectangle being intersected with this
+ * rectangle.
+ * @param bottom The bottom of the rectangle being intersected with this
+ * rectangle.
+ * @return true if the specified rectangle and this rectangle intersect
+ * (and this rectangle is then set to that intersection) else
+ * return false and do not change this rectangle.
+ */
+ public boolean intersect(float left, float top, float right, float bottom) {
+ if (this.left < right && left < this.right
+ && this.top < bottom && top < this.bottom) {
+ if (this.left < left) {
+ this.left = left;
+ }
+ if (this.top < top) {
+ this.top = top;
+ }
+ if (this.right > right) {
+ this.right = right;
+ }
+ if (this.bottom > bottom) {
+ this.bottom = bottom;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * If the specified rectangle intersects this rectangle, return true and set
+ * this rectangle to that intersection, otherwise return false and do not
+ * change this rectangle. No check is performed to see if either rectangle
+ * is empty. To just test for intersection, use intersects()
+ *
+ * @param r The rectangle being intersected with this rectangle.
+ * @return true if the specified rectangle and this rectangle intersect
+ * (and this rectangle is then set to that intersection) else
+ * return false and do not change this rectangle.
+ */
+ public boolean intersect(RectF r) {
+ return intersect(r.left, r.top, r.right, r.bottom);
+ }
+
+ /**
+ * If rectangles a and b intersect, return true and set this rectangle to
+ * that intersection, otherwise return false and do not change this
+ * rectangle. No check is performed to see if either rectangle is empty.
+ * To just test for intersection, use intersects()
+ *
+ * @param a The first rectangle being intersected with
+ * @param b The second rectangle being intersected with
+ * @return true iff the two specified rectangles intersect. If they do, set
+ * this rectangle to that intersection. If they do not, return
+ * false and do not change this rectangle.
+ */
+ public boolean setIntersect(RectF a, RectF b) {
+ if (a.left < b.right && b.left < a.right
+ && a.top < b.bottom && b.top < a.bottom) {
+ left = Math.max(a.left, b.left);
+ top = Math.max(a.top, b.top);
+ right = Math.min(a.right, b.right);
+ bottom = Math.min(a.bottom, b.bottom);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this rectangle intersects the specified rectangle.
+ * In no event is this rectangle modified. No check is performed to see
+ * if either rectangle is empty. To record the intersection, use intersect()
+ * or setIntersect().
+ *
+ * @param left The left side of the rectangle being tested for intersection
+ * @param top The top of the rectangle being tested for intersection
+ * @param right The right side of the rectangle being tested for
+ * intersection
+ * @param bottom The bottom of the rectangle being tested for intersection
+ * @return true iff the specified rectangle intersects this rectangle. In
+ * no event is this rectangle modified.
+ */
+ public boolean intersects(float left, float top, float right,
+ float bottom) {
+ return this.left < right && left < this.right
+ && this.top < bottom && top < this.bottom;
+ }
+
+ /**
+ * Returns true iff the two specified rectangles intersect. In no event are
+ * either of the rectangles modified. To record the intersection,
+ * use intersect() or setIntersect().
+ *
+ * @param a The first rectangle being tested for intersection
+ * @param b The second rectangle being tested for intersection
+ * @return true iff the two specified rectangles intersect. In no event are
+ * either of the rectangles modified.
+ */
+ public static boolean intersects(RectF a, RectF b) {
+ return a.left < b.right && b.left < a.right
+ && a.top < b.bottom && b.top < a.bottom;
+ }
+
+ /**
+ * Set the dst integer Rect by rounding this rectangle's coordinates
+ * to their nearest integer values.
+ */
+ public void round(Rect dst) {
+ dst.set(FastMath.round(left), FastMath.round(top),
+ FastMath.round(right), FastMath.round(bottom));
+ }
+
+ /**
+ * Set the dst integer Rect by rounding "out" this rectangle, choosing the
+ * floor of top and left, and the ceiling of right and bottom.
+ */
+ public void roundOut(Rect dst) {
+ dst.set((int) Math.floor(left), (int) Math.floor(top),
+ (int) Math.ceil(right), (int) Math.ceil(bottom));
+ }
+
+ /**
+ * Update this Rect to enclose itself and the specified rectangle. If the
+ * specified rectangle is empty, nothing is done. If this rectangle is empty
+ * it is set to the specified rectangle.
+ *
+ * @param left The left edge being unioned with this rectangle
+ * @param top The top edge being unioned with this rectangle
+ * @param right The right edge being unioned with this rectangle
+ * @param bottom The bottom edge being unioned with this rectangle
+ */
+ public void union(float left, float top, float right, float bottom) {
+ if ((left < right) && (top < bottom)) {
+ if ((this.left < this.right) && (this.top < this.bottom)) {
+ if (this.left > left)
+ this.left = left;
+ if (this.top > top)
+ this.top = top;
+ if (this.right < right)
+ this.right = right;
+ if (this.bottom < bottom)
+ this.bottom = bottom;
+ } else {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+ }
+ }
+
+ /**
+ * Update this Rect to enclose itself and the specified rectangle. If the
+ * specified rectangle is empty, nothing is done. If this rectangle is empty
+ * it is set to the specified rectangle.
+ *
+ * @param r The rectangle being unioned with this rectangle
+ */
+ public void union(RectF r) {
+ union(r.left, r.top, r.right, r.bottom);
+ }
+
+ /**
+ * Update this Rect to enclose itself and the [x,y] coordinate. There is no
+ * check to see that this rectangle is non-empty.
+ *
+ * @param x The x coordinate of the point to add to the rectangle
+ * @param y The y coordinate of the point to add to the rectangle
+ */
+ public void union(float x, float y) {
+ if (x < left) {
+ left = x;
+ } else if (x > right) {
+ right = x;
+ }
+ if (y < top) {
+ top = y;
+ } else if (y > bottom) {
+ bottom = y;
+ }
+ }
+
+ /**
+ * Swap top/bottom or left/right if there are flipped (i.e. left > right
+ * and/or top > bottom). This can be called if
+ * the edges are computed separately, and may have crossed over each other.
+ * If the edges are already correct (i.e. left <= right and top <= bottom)
+ * then nothing is done.
+ */
+ public void sort() {
+ if (left > right) {
+ float temp = left;
+ left = right;
+ right = temp;
+ }
+ if (top > bottom) {
+ float temp = top;
+ top = bottom;
+ bottom = temp;
+ }
+ }
+
+ /**
+ * Parcelable interface methods
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Write this rectangle to the specified parcel. To restore a rectangle from
+ * a parcel, use readFromParcel()
+ * @param out The parcel to write the rectangle's coordinates into
+ */
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeFloat(left);
+ out.writeFloat(top);
+ out.writeFloat(right);
+ out.writeFloat(bottom);
+ }
+
+ public static final Parcelable.Creator<RectF> CREATOR = new Parcelable.Creator<RectF>() {
+ /**
+ * Return a new rectangle from the data in the specified parcel.
+ */
+ public RectF createFromParcel(Parcel in) {
+ RectF r = new RectF();
+ r.readFromParcel(in);
+ return r;
+ }
+
+ /**
+ * Return an array of rectangles of the specified size.
+ */
+ public RectF[] newArray(int size) {
+ return new RectF[size];
+ }
+ };
+
+ /**
+ * Set the rectangle's coordinates from the data stored in the specified
+ * parcel. To write a rectangle to a parcel, call writeToParcel().
+ *
+ * @param in The parcel to read the rectangle's coordinates from
+ */
+ public void readFromParcel(Parcel in) {
+ left = in.readFloat();
+ top = in.readFloat();
+ right = in.readFloat();
+ bottom = in.readFloat();
+ }
+
+ /**
+ * Scales up the rect by the given scale.
+ * @hide
+ */
+ public void scale(float scale) {
+ if (scale != 1.0f) {
+ left = left * scale;
+ top = top * scale ;
+ right = right * scale;
+ bottom = bottom * scale;
+ }
+ }
+}
diff --git a/android/graphics/Region.java b/android/graphics/Region.java
new file mode 100644
index 0000000..dca6d9e
--- /dev/null
+++ b/android/graphics/Region.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pools.SynchronizedPool;
+
+public class Region implements Parcelable {
+
+ private static final int MAX_POOL_SIZE = 10;
+
+ private static final SynchronizedPool<Region> sPool =
+ new SynchronizedPool<Region>(MAX_POOL_SIZE);
+
+ /**
+ * @hide
+ */
+ public long mNativeRegion;
+
+ // the native values for these must match up with the enum in SkRegion.h
+ public enum Op {
+ DIFFERENCE(0),
+ INTERSECT(1),
+ UNION(2),
+ XOR(3),
+ REVERSE_DIFFERENCE(4),
+ REPLACE(5);
+
+ Op(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+
+ /**
+ * @hide
+ */
+ public final int nativeInt;
+ }
+
+ /** Create an empty region
+ */
+ public Region() {
+ this(nativeConstructor());
+ }
+
+ /** Return a copy of the specified region
+ */
+ public Region(Region region) {
+ this(nativeConstructor());
+ nativeSetRegion(mNativeRegion, region.mNativeRegion);
+ }
+
+ /** Return a region set to the specified rectangle
+ */
+ public Region(Rect r) {
+ mNativeRegion = nativeConstructor();
+ nativeSetRect(mNativeRegion, r.left, r.top, r.right, r.bottom);
+ }
+
+ /** Return a region set to the specified rectangle
+ */
+ public Region(int left, int top, int right, int bottom) {
+ mNativeRegion = nativeConstructor();
+ nativeSetRect(mNativeRegion, left, top, right, bottom);
+ }
+
+ /** Set the region to the empty region
+ */
+ public void setEmpty() {
+ nativeSetRect(mNativeRegion, 0, 0, 0, 0);
+ }
+
+ /** Set the region to the specified region.
+ */
+ public boolean set(Region region) {
+ nativeSetRegion(mNativeRegion, region.mNativeRegion);
+ return true;
+ }
+
+ /** Set the region to the specified rectangle
+ */
+ public boolean set(Rect r) {
+ return nativeSetRect(mNativeRegion, r.left, r.top, r.right, r.bottom);
+ }
+
+ /** Set the region to the specified rectangle
+ */
+ public boolean set(int left, int top, int right, int bottom) {
+ return nativeSetRect(mNativeRegion, left, top, right, bottom);
+ }
+
+ /**
+ * Set the region to the area described by the path and clip.
+ * Return true if the resulting region is non-empty. This produces a region
+ * that is identical to the pixels that would be drawn by the path
+ * (with no antialiasing).
+ */
+ public boolean setPath(Path path, Region clip) {
+ return nativeSetPath(mNativeRegion, path.readOnlyNI(), clip.mNativeRegion);
+ }
+
+ /**
+ * Return true if this region is empty
+ */
+ public native boolean isEmpty();
+
+ /**
+ * Return true if the region contains a single rectangle
+ */
+ public native boolean isRect();
+
+ /**
+ * Return true if the region contains more than one rectangle
+ */
+ public native boolean isComplex();
+
+ /**
+ * Return a new Rect set to the bounds of the region. If the region is
+ * empty, the Rect will be set to [0, 0, 0, 0]
+ */
+ public Rect getBounds() {
+ Rect r = new Rect();
+ nativeGetBounds(mNativeRegion, r);
+ return r;
+ }
+
+ /**
+ * Set the Rect to the bounds of the region. If the region is empty, the
+ * Rect will be set to [0, 0, 0, 0]
+ */
+ public boolean getBounds(Rect r) {
+ if (r == null) {
+ throw new NullPointerException();
+ }
+ return nativeGetBounds(mNativeRegion, r);
+ }
+
+ /**
+ * Return the boundary of the region as a new Path. If the region is empty,
+ * the path will also be empty.
+ */
+ public Path getBoundaryPath() {
+ Path path = new Path();
+ nativeGetBoundaryPath(mNativeRegion, path.mutateNI());
+ return path;
+ }
+
+ /**
+ * Set the path to the boundary of the region. If the region is empty, the
+ * path will also be empty.
+ */
+ public boolean getBoundaryPath(Path path) {
+ return nativeGetBoundaryPath(mNativeRegion, path.mutateNI());
+ }
+
+ /**
+ * Return true if the region contains the specified point
+ */
+ public native boolean contains(int x, int y);
+
+ /**
+ * Return true if the region is a single rectangle (not complex) and it
+ * contains the specified rectangle. Returning false is not a guarantee
+ * that the rectangle is not contained by this region, but return true is a
+ * guarantee that the rectangle is contained by this region.
+ */
+ public boolean quickContains(Rect r) {
+ return quickContains(r.left, r.top, r.right, r.bottom);
+ }
+
+ /**
+ * Return true if the region is a single rectangle (not complex) and it
+ * contains the specified rectangle. Returning false is not a guarantee
+ * that the rectangle is not contained by this region, but return true is a
+ * guarantee that the rectangle is contained by this region.
+ */
+ public native boolean quickContains(int left, int top, int right,
+ int bottom);
+
+ /**
+ * Return true if the region is empty, or if the specified rectangle does
+ * not intersect the region. Returning false is not a guarantee that they
+ * intersect, but returning true is a guarantee that they do not.
+ */
+ public boolean quickReject(Rect r) {
+ return quickReject(r.left, r.top, r.right, r.bottom);
+ }
+
+ /**
+ * Return true if the region is empty, or if the specified rectangle does
+ * not intersect the region. Returning false is not a guarantee that they
+ * intersect, but returning true is a guarantee that they do not.
+ */
+ public native boolean quickReject(int left, int top, int right, int bottom);
+
+ /**
+ * Return true if the region is empty, or if the specified region does not
+ * intersect the region. Returning false is not a guarantee that they
+ * intersect, but returning true is a guarantee that they do not.
+ */
+ public native boolean quickReject(Region rgn);
+
+ /**
+ * Translate the region by [dx, dy]. If the region is empty, do nothing.
+ */
+ public void translate(int dx, int dy) {
+ translate(dx, dy, null);
+ }
+
+ /**
+ * Set the dst region to the result of translating this region by [dx, dy].
+ * If this region is empty, then dst will be set to empty.
+ */
+ public native void translate(int dx, int dy, Region dst);
+
+ /**
+ * Scale the region by the given scale amount. This re-constructs new region by
+ * scaling the rects that this region consists of. New rectis are computed by scaling
+ * coordinates by float, then rounded by roundf() function to integers. This may results
+ * in less internal rects if 0 < scale < 1. Zero and Negative scale result in
+ * an empty region. If this region is empty, do nothing.
+ *
+ * @hide
+ */
+ public void scale(float scale) {
+ scale(scale, null);
+ }
+
+ /**
+ * Set the dst region to the result of scaling this region by the given scale amount.
+ * If this region is empty, then dst will be set to empty.
+ * @hide
+ */
+ public native void scale(float scale, Region dst);
+
+ public final boolean union(Rect r) {
+ return op(r, Op.UNION);
+ }
+
+ /**
+ * Perform the specified Op on this region and the specified rect. Return
+ * true if the result of the op is not empty.
+ */
+ public boolean op(Rect r, Op op) {
+ return nativeOp(mNativeRegion, r.left, r.top, r.right, r.bottom,
+ op.nativeInt);
+ }
+
+ /**
+ * Perform the specified Op on this region and the specified rect. Return
+ * true if the result of the op is not empty.
+ */
+ public boolean op(int left, int top, int right, int bottom, Op op) {
+ return nativeOp(mNativeRegion, left, top, right, bottom,
+ op.nativeInt);
+ }
+
+ /**
+ * Perform the specified Op on this region and the specified region. Return
+ * true if the result of the op is not empty.
+ */
+ public boolean op(Region region, Op op) {
+ return op(this, region, op);
+ }
+
+ /**
+ * Set this region to the result of performing the Op on the specified rect
+ * and region. Return true if the result is not empty.
+ */
+ public boolean op(Rect rect, Region region, Op op) {
+ return nativeOp(mNativeRegion, rect, region.mNativeRegion,
+ op.nativeInt);
+ }
+
+ /**
+ * Set this region to the result of performing the Op on the specified
+ * regions. Return true if the result is not empty.
+ */
+ public boolean op(Region region1, Region region2, Op op) {
+ return nativeOp(mNativeRegion, region1.mNativeRegion,
+ region2.mNativeRegion, op.nativeInt);
+ }
+
+ public String toString() {
+ return nativeToString(mNativeRegion);
+ }
+
+ /**
+ * @return An instance from a pool if such or a new one.
+ *
+ * @hide
+ */
+ public static Region obtain() {
+ Region region = sPool.acquire();
+ return (region != null) ? region : new Region();
+ }
+
+ /**
+ * @return An instance from a pool if such or a new one.
+ *
+ * @param other Region to copy values from for initialization.
+ *
+ * @hide
+ */
+ public static Region obtain(Region other) {
+ Region region = obtain();
+ region.set(other);
+ return region;
+ }
+
+ /**
+ * Recycles an instance.
+ *
+ * @hide
+ */
+ public void recycle() {
+ setEmpty();
+ sPool.release(this);
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+
+ public static final Parcelable.Creator<Region> CREATOR
+ = new Parcelable.Creator<Region>() {
+ /**
+ * Rebuild a Region previously stored with writeToParcel().
+ * @param p Parcel object to read the region from
+ * @return a new region created from the data in the parcel
+ */
+ public Region createFromParcel(Parcel p) {
+ long ni = nativeCreateFromParcel(p);
+ if (ni == 0) {
+ throw new RuntimeException();
+ }
+ return new Region(ni);
+ }
+ public Region[] newArray(int size) {
+ return new Region[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Write the region and its pixels to the parcel. The region can be
+ * rebuilt from the parcel by calling CREATOR.createFromParcel().
+ * @param p Parcel object to write the region data into
+ */
+ public void writeToParcel(Parcel p, int flags) {
+ if (!nativeWriteToParcel(mNativeRegion, p)) {
+ throw new RuntimeException();
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof Region)) {
+ return false;
+ }
+ Region peer = (Region) obj;
+ return nativeEquals(mNativeRegion, peer.mNativeRegion);
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ nativeDestructor(mNativeRegion);
+ mNativeRegion = 0;
+ } finally {
+ super.finalize();
+ }
+ }
+
+ Region(long ni) {
+ if (ni == 0) {
+ throw new RuntimeException();
+ }
+ mNativeRegion = ni;
+ }
+
+ /* add dummy parameter so constructor can be called from jni without
+ triggering 'not cloneable' exception */
+ private Region(long ni, int dummy) {
+ this(ni);
+ }
+
+ final long ni() {
+ return mNativeRegion;
+ }
+
+ private static native boolean nativeEquals(long native_r1, long native_r2);
+
+ private static native long nativeConstructor();
+ private static native void nativeDestructor(long native_region);
+
+ private static native void nativeSetRegion(long native_dst, long native_src);
+ private static native boolean nativeSetRect(long native_dst, int left,
+ int top, int right, int bottom);
+ private static native boolean nativeSetPath(long native_dst, long native_path,
+ long native_clip);
+ private static native boolean nativeGetBounds(long native_region, Rect rect);
+ private static native boolean nativeGetBoundaryPath(long native_region,
+ long native_path);
+
+ private static native boolean nativeOp(long native_dst, int left, int top,
+ int right, int bottom, int op);
+ private static native boolean nativeOp(long native_dst, Rect rect,
+ long native_region, int op);
+ private static native boolean nativeOp(long native_dst, long native_region1,
+ long native_region2, int op);
+
+ private static native long nativeCreateFromParcel(Parcel p);
+ private static native boolean nativeWriteToParcel(long native_region,
+ Parcel p);
+
+ private static native String nativeToString(long native_region);
+}
diff --git a/android/graphics/RegionIterator.java b/android/graphics/RegionIterator.java
new file mode 100644
index 0000000..443b23c
--- /dev/null
+++ b/android/graphics/RegionIterator.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+public class RegionIterator {
+
+ /**
+ * Construct an iterator for all of the rectangles in a region. This
+ * effectively makes a private copy of the region, so any subsequent edits
+ * to region will not affect the iterator.
+ *
+ * @param region the region that will be iterated
+ */
+ public RegionIterator(Region region) {
+ mNativeIter = nativeConstructor(region.ni());
+ }
+
+ /**
+ * Return the next rectangle in the region. If there are no more rectangles
+ * this returns false and r is unchanged. If there is at least one more,
+ * this returns true and r is set to that rectangle.
+ */
+ public final boolean next(Rect r) {
+ if (r == null) {
+ throw new NullPointerException("The Rect must be provided");
+ }
+ return nativeNext(mNativeIter, r);
+ }
+
+ protected void finalize() throws Throwable {
+ nativeDestructor(mNativeIter);
+ mNativeIter = 0; // Other finalizers can still call us.
+ }
+
+ private static native long nativeConstructor(long native_region);
+ private static native void nativeDestructor(long native_iter);
+ private static native boolean nativeNext(long native_iter, Rect r);
+
+ private long mNativeIter;
+}
+
diff --git a/android/graphics/Region_Delegate.java b/android/graphics/Region_Delegate.java
new file mode 100644
index 0000000..edb7025
--- /dev/null
+++ b/android/graphics/Region_Delegate.java
@@ -0,0 +1,483 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.os.Parcel;
+
+import java.awt.Rectangle;
+import java.awt.Shape;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Area;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * Delegate implementing the native methods of android.graphics.Region
+ *
+ * Through the layoutlib_create tool, the original native methods of Region have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original Region class.
+ *
+ * This also serve as a base class for all Region delegate classes.
+ *
+ * @see DelegateManager
+ *
+ */
+public class Region_Delegate {
+
+ // ---- delegate manager ----
+ protected static final DelegateManager<Region_Delegate> sManager =
+ new DelegateManager<Region_Delegate>(Region_Delegate.class);
+
+ // ---- delegate helper data ----
+
+ // ---- delegate data ----
+ private Area mArea = new Area();
+
+ // ---- Public Helper methods ----
+
+ public static Region_Delegate getDelegate(long nativeShader) {
+ return sManager.getDelegate(nativeShader);
+ }
+
+ public Area getJavaArea() {
+ return mArea;
+ }
+
+ /**
+ * Combines two {@link Shape} into another one (actually an {@link Area}), according
+ * to the given {@link Region.Op}.
+ *
+ * If the Op is not one that combines two shapes, then this return null
+ *
+ * @param shape1 the firt shape to combine which can be null if there's no original clip.
+ * @param shape2 the 2nd shape to combine
+ * @param regionOp the operande for the combine
+ * @return a new area or null.
+ */
+ public static Area combineShapes(Shape shape1, Shape shape2, int regionOp) {
+ if (regionOp == Region.Op.DIFFERENCE.nativeInt) {
+ // if shape1 is null (empty), then the result is null.
+ if (shape1 == null) {
+ return null;
+ }
+
+ // result is always a new area.
+ Area result = new Area(shape1);
+ result.subtract(shape2 instanceof Area ? (Area) shape2 : new Area(shape2));
+ return result;
+
+ } else if (regionOp == Region.Op.INTERSECT.nativeInt) {
+ // if shape1 is null, then the result is simply shape2.
+ if (shape1 == null) {
+ return new Area(shape2);
+ }
+
+ // result is always a new area.
+ Area result = new Area(shape1);
+ result.intersect(shape2 instanceof Area ? (Area) shape2 : new Area(shape2));
+ return result;
+
+ } else if (regionOp == Region.Op.UNION.nativeInt) {
+ // if shape1 is null, then the result is simply shape2.
+ if (shape1 == null) {
+ return new Area(shape2);
+ }
+
+ // result is always a new area.
+ Area result = new Area(shape1);
+ result.add(shape2 instanceof Area ? (Area) shape2 : new Area(shape2));
+ return result;
+
+ } else if (regionOp == Region.Op.XOR.nativeInt) {
+ // if shape1 is null, then the result is simply shape2
+ if (shape1 == null) {
+ return new Area(shape2);
+ }
+
+ // result is always a new area.
+ Area result = new Area(shape1);
+ result.exclusiveOr(shape2 instanceof Area ? (Area) shape2 : new Area(shape2));
+ return result;
+
+ } else if (regionOp == Region.Op.REVERSE_DIFFERENCE.nativeInt) {
+ // result is always a new area.
+ Area result = new Area(shape2);
+
+ if (shape1 != null) {
+ result.subtract(shape1 instanceof Area ? (Area) shape1 : new Area(shape1));
+ }
+
+ return result;
+ }
+
+ return null;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static boolean isEmpty(Region thisRegion) {
+ Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion);
+ if (regionDelegate == null) {
+ return true;
+ }
+
+ return regionDelegate.mArea.isEmpty();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean isRect(Region thisRegion) {
+ Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion);
+ if (regionDelegate == null) {
+ return true;
+ }
+
+ return regionDelegate.mArea.isRectangular();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean isComplex(Region thisRegion) {
+ Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion);
+ if (regionDelegate == null) {
+ return true;
+ }
+
+ return regionDelegate.mArea.isSingular() == false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean contains(Region thisRegion, int x, int y) {
+ Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion);
+ if (regionDelegate == null) {
+ return false;
+ }
+
+ return regionDelegate.mArea.contains(x, y);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean quickContains(Region thisRegion,
+ int left, int top, int right, int bottom) {
+ Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion);
+ if (regionDelegate == null) {
+ return false;
+ }
+
+ return regionDelegate.mArea.isRectangular() &&
+ regionDelegate.mArea.contains(left, top, right - left, bottom - top);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean quickReject(Region thisRegion,
+ int left, int top, int right, int bottom) {
+ Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion);
+ if (regionDelegate == null) {
+ return false;
+ }
+
+ return regionDelegate.mArea.isEmpty() ||
+ regionDelegate.mArea.intersects(left, top, right - left, bottom - top) == false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean quickReject(Region thisRegion, Region rgn) {
+ Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion);
+ if (regionDelegate == null) {
+ return false;
+ }
+
+ Region_Delegate targetRegionDelegate = sManager.getDelegate(rgn.mNativeRegion);
+ if (targetRegionDelegate == null) {
+ return false;
+ }
+
+ return regionDelegate.mArea.isEmpty() ||
+ regionDelegate.mArea.getBounds().intersects(
+ targetRegionDelegate.mArea.getBounds()) == false;
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void translate(Region thisRegion, int dx, int dy, Region dst) {
+ Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion);
+ if (regionDelegate == null) {
+ return;
+ }
+
+ Region_Delegate targetRegionDelegate = sManager.getDelegate(dst.mNativeRegion);
+ if (targetRegionDelegate == null) {
+ return;
+ }
+
+ if (regionDelegate.mArea.isEmpty()) {
+ targetRegionDelegate.mArea = new Area();
+ } else {
+ targetRegionDelegate.mArea = new Area(regionDelegate.mArea);
+ AffineTransform mtx = new AffineTransform();
+ mtx.translate(dx, dy);
+ targetRegionDelegate.mArea.transform(mtx);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void scale(Region thisRegion, float scale, Region dst) {
+ Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion);
+ if (regionDelegate == null) {
+ return;
+ }
+
+ Region_Delegate targetRegionDelegate = sManager.getDelegate(dst.mNativeRegion);
+ if (targetRegionDelegate == null) {
+ return;
+ }
+
+ if (regionDelegate.mArea.isEmpty()) {
+ targetRegionDelegate.mArea = new Area();
+ } else {
+ targetRegionDelegate.mArea = new Area(regionDelegate.mArea);
+ AffineTransform mtx = new AffineTransform();
+ mtx.scale(scale, scale);
+ targetRegionDelegate.mArea.transform(mtx);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeConstructor() {
+ Region_Delegate newDelegate = new Region_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeDestructor(long native_region) {
+ sManager.removeJavaReferenceFor(native_region);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeSetRegion(long native_dst, long native_src) {
+ Region_Delegate dstRegion = sManager.getDelegate(native_dst);
+ if (dstRegion == null) {
+ return;
+ }
+
+ Region_Delegate srcRegion = sManager.getDelegate(native_src);
+ if (srcRegion == null) {
+ return;
+ }
+
+ dstRegion.mArea.reset();
+ dstRegion.mArea.add(srcRegion.mArea);
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeSetRect(long native_dst,
+ int left, int top, int right, int bottom) {
+ Region_Delegate dstRegion = sManager.getDelegate(native_dst);
+ if (dstRegion == null) {
+ return true;
+ }
+
+ dstRegion.mArea = new Area(new Rectangle2D.Float(left, top, right - left, bottom - top));
+ return dstRegion.mArea.getBounds().isEmpty() == false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeSetPath(long native_dst, long native_path, long native_clip) {
+ Region_Delegate dstRegion = sManager.getDelegate(native_dst);
+ if (dstRegion == null) {
+ return true;
+ }
+
+ Path_Delegate path = Path_Delegate.getDelegate(native_path);
+ if (path == null) {
+ return true;
+ }
+
+ dstRegion.mArea = new Area(path.getJavaShape());
+
+ Region_Delegate clip = sManager.getDelegate(native_clip);
+ if (clip != null) {
+ dstRegion.mArea.subtract(clip.getJavaArea());
+ }
+
+ return dstRegion.mArea.getBounds().isEmpty() == false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeGetBounds(long native_region, Rect rect) {
+ Region_Delegate region = sManager.getDelegate(native_region);
+ if (region == null) {
+ return true;
+ }
+
+ Rectangle bounds = region.mArea.getBounds();
+ if (bounds.isEmpty()) {
+ rect.left = rect.top = rect.right = rect.bottom = 0;
+ return false;
+ }
+
+ rect.left = bounds.x;
+ rect.top = bounds.y;
+ rect.right = bounds.x + bounds.width;
+ rect.bottom = bounds.y + bounds.height;
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeGetBoundaryPath(long native_region, long native_path) {
+ Region_Delegate region = sManager.getDelegate(native_region);
+ if (region == null) {
+ return false;
+ }
+
+ Path_Delegate path = Path_Delegate.getDelegate(native_path);
+ if (path == null) {
+ return false;
+ }
+
+ if (region.mArea.isEmpty()) {
+ path.reset();
+ return false;
+ }
+
+ path.setPathIterator(region.mArea.getPathIterator(new AffineTransform()));
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeOp(long native_dst,
+ int left, int top, int right, int bottom, int op) {
+ Region_Delegate region = sManager.getDelegate(native_dst);
+ if (region == null) {
+ return false;
+ }
+
+ region.mArea = combineShapes(region.mArea,
+ new Rectangle2D.Float(left, top, right - left, bottom - top), op);
+
+ assert region.mArea != null;
+ if (region.mArea != null) {
+ region.mArea = new Area();
+ }
+
+ return region.mArea.getBounds().isEmpty() == false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeOp(long native_dst, Rect rect, long native_region, int op) {
+ Region_Delegate region = sManager.getDelegate(native_dst);
+ if (region == null) {
+ return false;
+ }
+
+ region.mArea = combineShapes(region.mArea,
+ new Rectangle2D.Float(rect.left, rect.top, rect.width(), rect.height()), op);
+
+ assert region.mArea != null;
+ if (region.mArea != null) {
+ region.mArea = new Area();
+ }
+
+ return region.mArea.getBounds().isEmpty() == false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeOp(long native_dst,
+ long native_region1, long native_region2, int op) {
+ Region_Delegate dstRegion = sManager.getDelegate(native_dst);
+ if (dstRegion == null) {
+ return true;
+ }
+
+ Region_Delegate region1 = sManager.getDelegate(native_region1);
+ if (region1 == null) {
+ return false;
+ }
+
+ Region_Delegate region2 = sManager.getDelegate(native_region2);
+ if (region2 == null) {
+ return false;
+ }
+
+ dstRegion.mArea = combineShapes(region1.mArea, region2.mArea, op);
+
+ assert dstRegion.mArea != null;
+ if (dstRegion.mArea != null) {
+ dstRegion.mArea = new Area();
+ }
+
+ return dstRegion.mArea.getBounds().isEmpty() == false;
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreateFromParcel(Parcel p) {
+ // This is only called by Region.CREATOR (Parcelable.Creator<Region>), which is only
+ // used during aidl call so really this should not be called.
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "AIDL is not suppored, and therefore Regions cannot be created from parcels.",
+ null /*data*/);
+ return 0;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeWriteToParcel(long native_region,
+ Parcel p) {
+ // This is only called when sending a region through aidl, so really this should not
+ // be called.
+ Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
+ "AIDL is not suppored, and therefore Regions cannot be written to parcels.",
+ null /*data*/);
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nativeEquals(long native_r1, long native_r2) {
+ Region_Delegate region1 = sManager.getDelegate(native_r1);
+ if (region1 == null) {
+ return false;
+ }
+
+ Region_Delegate region2 = sManager.getDelegate(native_r2);
+ if (region2 == null) {
+ return false;
+ }
+
+ return region1.mArea.equals(region2.mArea);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static String nativeToString(long native_region) {
+ Region_Delegate region = sManager.getDelegate(native_region);
+ if (region == null) {
+ return "not found";
+ }
+
+ return region.mArea.toString();
+ }
+
+ // ---- Private delegate/helper methods ----
+
+}
diff --git a/android/graphics/RoundRectangle.java b/android/graphics/RoundRectangle.java
new file mode 100644
index 0000000..736f03e
--- /dev/null
+++ b/android/graphics/RoundRectangle.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2016 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 android.graphics;
+
+import java.awt.geom.AffineTransform;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Rectangle2D;
+import java.awt.geom.RectangularShape;
+import java.awt.geom.RoundRectangle2D;
+import java.util.EnumSet;
+import java.util.NoSuchElementException;
+
+/**
+ * Defines a rectangle with rounded corners, where the sizes of the corners
+ * are potentially different.
+ */
+public class RoundRectangle extends RectangularShape {
+ public double x;
+ public double y;
+ public double width;
+ public double height;
+ public double ulWidth;
+ public double ulHeight;
+ public double urWidth;
+ public double urHeight;
+ public double lrWidth;
+ public double lrHeight;
+ public double llWidth;
+ public double llHeight;
+
+ private enum Zone {
+ CLOSE_OUTSIDE,
+ CLOSE_INSIDE,
+ MIDDLE,
+ FAR_INSIDE,
+ FAR_OUTSIDE
+ }
+
+ private final EnumSet<Zone> close = EnumSet.of(Zone.CLOSE_OUTSIDE, Zone.CLOSE_INSIDE);
+ private final EnumSet<Zone> far = EnumSet.of(Zone.FAR_OUTSIDE, Zone.FAR_INSIDE);
+
+ /**
+ * @param cornerDimensions array of 8 floating-point number corresponding to the width and
+ * the height of each corner in the following order: upper-left, upper-right, lower-right,
+ * lower-left. It assumes for the size the same convention as {@link RoundRectangle2D}, that
+ * is that the width and height of a corner correspond to the total width and height of the
+ * ellipse that corner is a quarter of.
+ */
+ public RoundRectangle(float x, float y, float width, float height, float[] cornerDimensions) {
+ assert cornerDimensions.length == 8 : "The array of corner dimensions must have eight " +
+ "elements";
+
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+
+ float[] dimensions = cornerDimensions.clone();
+ // If a value is negative, the corresponding corner is squared
+ for (int i = 0; i < dimensions.length; i += 2) {
+ if (dimensions[i] < 0 || dimensions[i + 1] < 0) {
+ dimensions[i] = 0;
+ dimensions[i + 1] = 0;
+ }
+ }
+
+ double topCornerWidth = (dimensions[0] + dimensions[2]) / 2d;
+ double bottomCornerWidth = (dimensions[4] + dimensions[6]) / 2d;
+ double leftCornerHeight = (dimensions[1] + dimensions[7]) / 2d;
+ double rightCornerHeight = (dimensions[3] + dimensions[5]) / 2d;
+
+ // Rescale the corner dimensions if they are bigger than the rectangle
+ double scale = Math.min(1.0, width / topCornerWidth);
+ scale = Math.min(scale, width / bottomCornerWidth);
+ scale = Math.min(scale, height / leftCornerHeight);
+ scale = Math.min(scale, height / rightCornerHeight);
+
+ this.ulWidth = dimensions[0] * scale;
+ this.ulHeight = dimensions[1] * scale;
+ this.urWidth = dimensions[2] * scale;
+ this.urHeight = dimensions[3] * scale;
+ this.lrWidth = dimensions[4] * scale;
+ this.lrHeight = dimensions[5] * scale;
+ this.llWidth = dimensions[6] * scale;
+ this.llHeight = dimensions[7] * scale;
+ }
+
+ @Override
+ public double getX() {
+ return x;
+ }
+
+ @Override
+ public double getY() {
+ return y;
+ }
+
+ @Override
+ public double getWidth() {
+ return width;
+ }
+
+ @Override
+ public double getHeight() {
+ return height;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return (width <= 0d) || (height <= 0d);
+ }
+
+ @Override
+ public void setFrame(double x, double y, double w, double h) {
+ this.x = x;
+ this.y = y;
+ this.width = w;
+ this.height = h;
+ }
+
+ @Override
+ public Rectangle2D getBounds2D() {
+ return new Rectangle2D.Double(x, y, width, height);
+ }
+
+ @Override
+ public boolean contains(double x, double y) {
+ if (isEmpty()) {
+ return false;
+ }
+
+ double x0 = getX();
+ double y0 = getY();
+ double x1 = x0 + getWidth();
+ double y1 = y0 + getHeight();
+ // Check for trivial rejection - point is outside bounding rectangle
+ if (x < x0 || y < y0 || x >= x1 || y >= y1) {
+ return false;
+ }
+
+ double insideTopX0 = x0 + ulWidth / 2d;
+ double insideLeftY0 = y0 + ulHeight / 2d;
+ if (x < insideTopX0 && y < insideLeftY0) {
+ // In the upper-left corner
+ return isInsideCorner(x - insideTopX0, y - insideLeftY0, ulWidth / 2d, ulHeight / 2d);
+ }
+
+ double insideTopX1 = x1 - urWidth / 2d;
+ double insideRightY0 = y0 + urHeight / 2d;
+ if (x > insideTopX1 && y < insideRightY0) {
+ // In the upper-right corner
+ return isInsideCorner(x - insideTopX1, y - insideRightY0, urWidth / 2d, urHeight / 2d);
+ }
+
+ double insideBottomX1 = x1 - lrWidth / 2d;
+ double insideRightY1 = y1 - lrHeight / 2d;
+ if (x > insideBottomX1 && y > insideRightY1) {
+ // In the lower-right corner
+ return isInsideCorner(x - insideBottomX1, y - insideRightY1, lrWidth / 2d,
+ lrHeight / 2d);
+ }
+
+ double insideBottomX0 = x0 + llWidth / 2d;
+ double insideLeftY1 = y1 - llHeight / 2d;
+ if (x < insideBottomX0 && y > insideLeftY1) {
+ // In the lower-left corner
+ return isInsideCorner(x - insideBottomX0, y - insideLeftY1, llWidth / 2d,
+ llHeight / 2d);
+ }
+
+ // In the central part of the rectangle
+ return true;
+ }
+
+ private boolean isInsideCorner(double x, double y, double width, double height) {
+ double squareDist = height * height * x * x + width * width * y * y;
+ return squareDist <= width * width * height * height;
+ }
+
+ private Zone classify(double coord, double side1, double arcSize1, double side2,
+ double arcSize2) {
+ if (coord < side1) {
+ return Zone.CLOSE_OUTSIDE;
+ } else if (coord < side1 + arcSize1) {
+ return Zone.CLOSE_INSIDE;
+ } else if (coord < side2 - arcSize2) {
+ return Zone.MIDDLE;
+ } else if (coord < side2) {
+ return Zone.FAR_INSIDE;
+ } else {
+ return Zone.FAR_OUTSIDE;
+ }
+ }
+
+ public boolean intersects(double x, double y, double w, double h) {
+ if (isEmpty() || w <= 0 || h <= 0) {
+ return false;
+ }
+ double x0 = getX();
+ double y0 = getY();
+ double x1 = x0 + getWidth();
+ double y1 = y0 + getHeight();
+ // Check for trivial rejection - bounding rectangles do not intersect
+ if (x + w <= x0 || x >= x1 || y + h <= y0 || y >= y1) {
+ return false;
+ }
+
+ double maxLeftCornerWidth = Math.max(ulWidth, llWidth) / 2d;
+ double maxRightCornerWidth = Math.max(urWidth, lrWidth) / 2d;
+ double maxUpperCornerHeight = Math.max(ulHeight, urHeight) / 2d;
+ double maxLowerCornerHeight = Math.max(llHeight, lrHeight) / 2d;
+ Zone x0class = classify(x, x0, maxLeftCornerWidth, x1, maxRightCornerWidth);
+ Zone x1class = classify(x + w, x0, maxLeftCornerWidth, x1, maxRightCornerWidth);
+ Zone y0class = classify(y, y0, maxUpperCornerHeight, y1, maxLowerCornerHeight);
+ Zone y1class = classify(y + h, y0, maxUpperCornerHeight, y1, maxLowerCornerHeight);
+
+ // Trivially accept if any point is inside inner rectangle
+ if (x0class == Zone.MIDDLE || x1class == Zone.MIDDLE || y0class == Zone.MIDDLE || y1class == Zone.MIDDLE) {
+ return true;
+ }
+ // Trivially accept if either edge spans inner rectangle
+ if ((close.contains(x0class) && far.contains(x1class)) || (close.contains(y0class) &&
+ far.contains(y1class))) {
+ return true;
+ }
+
+ // Since neither edge spans the center, then one of the corners
+ // must be in one of the rounded edges. We detect this case if
+ // a [xy]0class is 3 or a [xy]1class is 1. One of those two cases
+ // must be true for each direction.
+ // We now find a "nearest point" to test for being inside a rounded
+ // corner.
+ if (x1class == Zone.CLOSE_INSIDE && y1class == Zone.CLOSE_INSIDE) {
+ // Potentially in upper-left corner
+ x = x + w - x0 - ulWidth / 2d;
+ y = y + h - y0 - ulHeight / 2d;
+ return x > 0 || y > 0 || isInsideCorner(x, y, ulWidth / 2d, ulHeight / 2d);
+ }
+ if (x1class == Zone.CLOSE_INSIDE) {
+ // Potentially in lower-left corner
+ x = x + w - x0 - llWidth / 2d;
+ y = y - y1 + llHeight / 2d;
+ return x > 0 || y < 0 || isInsideCorner(x, y, llWidth / 2d, llHeight / 2d);
+ }
+ if (y1class == Zone.CLOSE_INSIDE) {
+ //Potentially in the upper-right corner
+ x = x - x1 + urWidth / 2d;
+ y = y + h - y0 - urHeight / 2d;
+ return x < 0 || y > 0 || isInsideCorner(x, y, urWidth / 2d, urHeight / 2d);
+ }
+ // Potentially in the lower-right corner
+ x = x - x1 + lrWidth / 2d;
+ y = y - y1 + lrHeight / 2d;
+ return x < 0 || y < 0 || isInsideCorner(x, y, lrWidth / 2d, lrHeight / 2d);
+ }
+
+ @Override
+ public boolean contains(double x, double y, double w, double h) {
+ if (isEmpty() || w <= 0 || h <= 0) {
+ return false;
+ }
+ return (contains(x, y) &&
+ contains(x + w, y) &&
+ contains(x, y + h) &&
+ contains(x + w, y + h));
+ }
+
+ @Override
+ public PathIterator getPathIterator(final AffineTransform at) {
+ return new PathIterator() {
+ int index;
+
+ // ArcIterator.btan(Math.PI/2)
+ public static final double CtrlVal = 0.5522847498307933;
+ private final double ncv = 1.0 - CtrlVal;
+
+ // Coordinates of control points for Bezier curves approximating the straight lines
+ // and corners of the rounded rectangle.
+ private final double[][] ctrlpts = {
+ {0.0, 0.0, 0.0, ulHeight},
+ {0.0, 0.0, 1.0, -llHeight},
+ {0.0, 0.0, 1.0, -llHeight * ncv, 0.0, ncv * llWidth, 1.0, 0.0, 0.0, llWidth,
+ 1.0, 0.0},
+ {1.0, -lrWidth, 1.0, 0.0},
+ {1.0, -lrWidth * ncv, 1.0, 0.0, 1.0, 0.0, 1.0, -lrHeight * ncv, 1.0, 0.0, 1.0,
+ -lrHeight},
+ {1.0, 0.0, 0.0, urHeight},
+ {1.0, 0.0, 0.0, ncv * urHeight, 1.0, -urWidth * ncv, 0.0, 0.0, 1.0, -urWidth,
+ 0.0, 0.0},
+ {0.0, ulWidth, 0.0, 0.0},
+ {0.0, ncv * ulWidth, 0.0, 0.0, 0.0, 0.0, 0.0, ncv * ulHeight, 0.0, 0.0, 0.0,
+ ulHeight},
+ {}
+ };
+ private final int[] types = {
+ SEG_MOVETO,
+ SEG_LINETO, SEG_CUBICTO,
+ SEG_LINETO, SEG_CUBICTO,
+ SEG_LINETO, SEG_CUBICTO,
+ SEG_LINETO, SEG_CUBICTO,
+ SEG_CLOSE,
+ };
+
+ @Override
+ public int getWindingRule() {
+ return WIND_NON_ZERO;
+ }
+
+ @Override
+ public boolean isDone() {
+ return index >= ctrlpts.length;
+ }
+
+ @Override
+ public void next() {
+ index++;
+ }
+
+ @Override
+ public int currentSegment(float[] coords) {
+ if (isDone()) {
+ throw new NoSuchElementException("roundrect iterator out of bounds");
+ }
+ int nc = 0;
+ double ctrls[] = ctrlpts[index];
+ for (int i = 0; i < ctrls.length; i += 4) {
+ coords[nc++] = (float) (x + ctrls[i] * width + ctrls[i + 1] / 2d);
+ coords[nc++] = (float) (y + ctrls[i + 2] * height + ctrls[i + 3] / 2d);
+ }
+ if (at != null) {
+ at.transform(coords, 0, coords, 0, nc / 2);
+ }
+ return types[index];
+ }
+
+ @Override
+ public int currentSegment(double[] coords) {
+ if (isDone()) {
+ throw new NoSuchElementException("roundrect iterator out of bounds");
+ }
+ int nc = 0;
+ double ctrls[] = ctrlpts[index];
+ for (int i = 0; i < ctrls.length; i += 4) {
+ coords[nc++] = x + ctrls[i] * width + ctrls[i + 1] / 2d;
+ coords[nc++] = y + ctrls[i + 2] * height + ctrls[i + 3] / 2d;
+ }
+ if (at != null) {
+ at.transform(coords, 0, coords, 0, nc / 2);
+ }
+ return types[index];
+ }
+ };
+ }
+}
diff --git a/android/graphics/Shader.java b/android/graphics/Shader.java
new file mode 100644
index 0000000..0209cea
--- /dev/null
+++ b/android/graphics/Shader.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import libcore.util.NativeAllocationRegistry;
+
+/**
+ * Shader is the based class for objects that return horizontal spans of colors
+ * during drawing. A subclass of Shader is installed in a Paint calling
+ * paint.setShader(shader). After that any object (other than a bitmap) that is
+ * drawn with that paint will get its color(s) from the shader.
+ */
+public class Shader {
+
+ private static class NoImagePreloadHolder {
+ public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
+ Shader.class.getClassLoader(), nativeGetFinalizer(), 50);
+ }
+
+ /**
+ * @deprecated Use subclass constructors directly instead.
+ */
+ @Deprecated
+ public Shader() {}
+
+ /**
+ * Current native shader instance. Created and updated lazily when {@link #getNativeInstance()}
+ * is called - otherwise may be out of date with java setters/properties.
+ */
+ private long mNativeInstance;
+ // Runnable to do immediate destruction
+ private Runnable mCleaner;
+
+ /**
+ * Current matrix - always set to null if local matrix is identity.
+ */
+ private Matrix mLocalMatrix;
+
+ public enum TileMode {
+ /**
+ * replicate the edge color if the shader draws outside of its
+ * original bounds
+ */
+ CLAMP (0),
+ /**
+ * repeat the shader's image horizontally and vertically
+ */
+ REPEAT (1),
+ /**
+ * repeat the shader's image horizontally and vertically, alternating
+ * mirror images so that adjacent images always seam
+ */
+ MIRROR (2);
+
+ TileMode(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ /**
+ * Return true if the shader has a non-identity local matrix.
+ * @param localM Set to the local matrix of the shader, if the shader's matrix is non-null.
+ * @return true if the shader has a non-identity local matrix
+ */
+ public boolean getLocalMatrix(@NonNull Matrix localM) {
+ if (mLocalMatrix != null) {
+ localM.set(mLocalMatrix);
+ return true; // presence of mLocalMatrix means it's not identity
+ }
+ return false;
+ }
+
+ /**
+ * Set the shader's local matrix. Passing null will reset the shader's
+ * matrix to identity. If the matrix has scale value as 0, the drawing
+ * result is undefined.
+ *
+ * @param localM The shader's new local matrix, or null to specify identity
+ */
+ public void setLocalMatrix(@Nullable Matrix localM) {
+ if (localM == null || localM.isIdentity()) {
+ if (mLocalMatrix != null) {
+ mLocalMatrix = null;
+ discardNativeInstance();
+ }
+ } else {
+ if (mLocalMatrix == null) {
+ mLocalMatrix = new Matrix(localM);
+ discardNativeInstance();
+ } else if (!mLocalMatrix.equals(localM)) {
+ mLocalMatrix.set(localM);
+ discardNativeInstance();
+ }
+ }
+ }
+
+ long createNativeInstance(long nativeMatrix) {
+ return 0;
+ }
+
+ /** @hide */
+ protected final void discardNativeInstance() {
+ if (mNativeInstance != 0) {
+ mCleaner.run();
+ mCleaner = null;
+ mNativeInstance = 0;
+ }
+ }
+
+ /**
+ * Callback for subclasses to call {@link #discardNativeInstance()} if the most recently
+ * constructed native instance is no longer valid.
+ * @hide
+ */
+ protected void verifyNativeInstance() {
+ }
+
+ /**
+ * @hide
+ */
+ protected Shader copy() {
+ final Shader copy = new Shader();
+ copyLocalMatrix(copy);
+ return copy;
+ }
+
+ /**
+ * @hide
+ */
+ protected void copyLocalMatrix(Shader dest) {
+ dest.mLocalMatrix.set(mLocalMatrix);
+ }
+
+ /**
+ * @hide
+ */
+ public final long getNativeInstance() {
+ // verify mNativeInstance is valid
+ verifyNativeInstance();
+
+ if (mNativeInstance == 0) {
+ mNativeInstance = createNativeInstance(mLocalMatrix == null
+ ? 0 : mLocalMatrix.native_instance);
+ mCleaner = NoImagePreloadHolder.sRegistry.registerNativeAllocation(
+ this, mNativeInstance);
+ }
+ return mNativeInstance;
+ }
+
+ private static native long nativeGetFinalizer();
+
+}
+
diff --git a/android/graphics/Shader_Delegate.java b/android/graphics/Shader_Delegate.java
new file mode 100644
index 0000000..eefa929
--- /dev/null
+++ b/android/graphics/Shader_Delegate.java
@@ -0,0 +1,110 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.graphics.Shader.TileMode;
+
+import libcore.util.NativeAllocationRegistry_Delegate;
+
+/**
+ * Delegate implementing the native methods of android.graphics.Shader
+ *
+ * Through the layoutlib_create tool, the original native methods of Shader have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original Shader class.
+ *
+ * This also serve as a base class for all Shader delegate classes.
+ *
+ * @see DelegateManager
+ *
+ */
+public abstract class Shader_Delegate {
+
+ // ---- delegate manager ----
+ protected static final DelegateManager<Shader_Delegate> sManager =
+ new DelegateManager<Shader_Delegate>(Shader_Delegate.class);
+ private static long sFinalizer = -1;
+
+ // ---- delegate helper data ----
+
+ // ---- delegate data ----
+ private Matrix_Delegate mLocalMatrix = null;
+
+ // ---- Public Helper methods ----
+
+ public static Shader_Delegate getDelegate(long nativeShader) {
+ return sManager.getDelegate(nativeShader);
+ }
+
+ /**
+ * Returns the {@link TileMode} matching the given int.
+ * @param tileMode the tile mode int value
+ * @return the TileMode enum.
+ */
+ public static TileMode getTileMode(int tileMode) {
+ for (TileMode tm : TileMode.values()) {
+ if (tm.nativeInt == tileMode) {
+ return tm;
+ }
+ }
+
+ assert false;
+ return TileMode.CLAMP;
+ }
+
+ public abstract java.awt.Paint getJavaPaint();
+ public abstract boolean isSupported();
+ public abstract String getSupportMessage();
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeGetFinalizer() {
+ synchronized (Shader_Delegate.class) {
+ if (sFinalizer == -1) {
+ sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
+ sManager::removeJavaReferenceFor);
+ }
+ }
+ return sFinalizer;
+ }
+
+ // ---- Private delegate/helper methods ----
+
+ protected Shader_Delegate(long nativeMatrix) {
+ setLocalMatrix(nativeMatrix);
+ }
+
+ public void setLocalMatrix(long nativeMatrix) {
+ mLocalMatrix = Matrix_Delegate.getDelegate(nativeMatrix);
+ }
+
+ protected java.awt.geom.AffineTransform getLocalMatrix() {
+ if (mLocalMatrix != null) {
+ return mLocalMatrix.getAffineTransform();
+ }
+
+ return new java.awt.geom.AffineTransform();
+ }
+
+}
diff --git a/android/graphics/SumPathEffect.java b/android/graphics/SumPathEffect.java
new file mode 100644
index 0000000..8fedc31
--- /dev/null
+++ b/android/graphics/SumPathEffect.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+public class SumPathEffect extends PathEffect {
+
+ /**
+ * Construct a PathEffect whose effect is to apply two effects, in sequence.
+ * (e.g. first(path) + second(path))
+ */
+ public SumPathEffect(PathEffect first, PathEffect second) {
+ native_instance = nativeCreate(first.native_instance,
+ second.native_instance);
+ }
+
+ private static native long nativeCreate(long first, long second);
+}
+
diff --git a/android/graphics/SumPathEffect_Delegate.java b/android/graphics/SumPathEffect_Delegate.java
new file mode 100644
index 0000000..6d2e9b4
--- /dev/null
+++ b/android/graphics/SumPathEffect_Delegate.java
@@ -0,0 +1,71 @@
+/*
+ * 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 android.graphics;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.awt.Stroke;
+
+/**
+ * Delegate implementing the native methods of android.graphics.SumPathEffect
+ *
+ * Through the layoutlib_create tool, the original native methods of SumPathEffect have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original SumPathEffect class.
+ *
+ * Because this extends {@link PathEffect_Delegate}, there's no need to use a {@link DelegateManager},
+ * as all the Shader classes will be added to the manager owned by {@link PathEffect_Delegate}.
+ *
+ * @see PathEffect_Delegate
+ *
+ */
+public class SumPathEffect_Delegate extends PathEffect_Delegate {
+
+ // ---- delegate data ----
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public Stroke getStroke(Paint_Delegate paint) {
+ // FIXME
+ return null;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return false;
+ }
+
+ @Override
+ public String getSupportMessage() {
+ return "Sum Path Effects are not supported in Layout Preview mode.";
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate(long first, long second) {
+ SumPathEffect_Delegate newDelegate = new SumPathEffect_Delegate();
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ // ---- Private delegate/helper methods ----
+}
diff --git a/android/graphics/SurfaceTexture.java b/android/graphics/SurfaceTexture.java
new file mode 100644
index 0000000..97edf22
--- /dev/null
+++ b/android/graphics/SurfaceTexture.java
@@ -0,0 +1,408 @@
+/*
+ * 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 android.graphics;
+
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.Surface;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Captures frames from an image stream as an OpenGL ES texture.
+ *
+ * <p>The image stream may come from either camera preview or video decode. A
+ * {@link android.view.Surface} created from a SurfaceTexture can be used as an output
+ * destination for the {@link android.hardware.camera2}, {@link android.media.MediaCodec},
+ * {@link android.media.MediaPlayer}, and {@link android.renderscript.Allocation} APIs.
+ * When {@link #updateTexImage} is called, the contents of the texture object specified
+ * when the SurfaceTexture was created are updated to contain the most recent image from the image
+ * stream. This may cause some frames of the stream to be skipped.
+ *
+ * <p>A SurfaceTexture may also be used in place of a SurfaceHolder when specifying the output
+ * destination of the older {@link android.hardware.Camera} API. Doing so will cause all the
+ * frames from the image stream to be sent to the SurfaceTexture object rather than to the device's
+ * display.
+ *
+ * <p>When sampling from the texture one should first transform the texture coordinates using the
+ * matrix queried via {@link #getTransformMatrix(float[])}. The transform matrix may change each
+ * time {@link #updateTexImage} is called, so it should be re-queried each time the texture image
+ * is updated.
+ * This matrix transforms traditional 2D OpenGL ES texture coordinate column vectors of the form (s,
+ * t, 0, 1) where s and t are on the inclusive interval [0, 1] to the proper sampling location in
+ * the streamed texture. This transform compensates for any properties of the image stream source
+ * that cause it to appear different from a traditional OpenGL ES texture. For example, sampling
+ * from the bottom left corner of the image can be accomplished by transforming the column vector
+ * (0, 0, 0, 1) using the queried matrix, while sampling from the top right corner of the image can
+ * be done by transforming (1, 1, 0, 1).
+ *
+ * <p>The texture object uses the GL_TEXTURE_EXTERNAL_OES texture target, which is defined by the
+ * <a href="http://www.khronos.org/registry/gles/extensions/OES/OES_EGL_image_external.txt">
+ * GL_OES_EGL_image_external</a> OpenGL ES extension. This limits how the texture may be used.
+ * Each time the texture is bound it must be bound to the GL_TEXTURE_EXTERNAL_OES target rather than
+ * the GL_TEXTURE_2D target. Additionally, any OpenGL ES 2.0 shader that samples from the texture
+ * must declare its use of this extension using, for example, an "#extension
+ * GL_OES_EGL_image_external : require" directive. Such shaders must also access the texture using
+ * the samplerExternalOES GLSL sampler type.
+ *
+ * <p>SurfaceTexture objects may be created on any thread. {@link #updateTexImage} may only be
+ * called on the thread with the OpenGL ES context that contains the texture object. The
+ * frame-available callback is called on an arbitrary thread, so unless special care is taken {@link
+ * #updateTexImage} should not be called directly from the callback.
+ */
+public class SurfaceTexture {
+ private final Looper mCreatorLooper;
+ private Handler mOnFrameAvailableHandler;
+
+ /**
+ * These fields are used by native code, do not access or modify.
+ */
+ private long mSurfaceTexture;
+ private long mProducer;
+ private long mFrameAvailableListener;
+
+ private boolean mIsSingleBuffered;
+
+ /**
+ * Callback interface for being notified that a new stream frame is available.
+ */
+ public interface OnFrameAvailableListener {
+ void onFrameAvailable(SurfaceTexture surfaceTexture);
+ }
+
+ /**
+ * Exception thrown when a SurfaceTexture couldn't be created or resized.
+ *
+ * @deprecated No longer thrown. {@link android.view.Surface.OutOfResourcesException}
+ * is used instead.
+ */
+ @SuppressWarnings("serial")
+ @Deprecated
+ public static class OutOfResourcesException extends Exception {
+ public OutOfResourcesException() {
+ }
+ public OutOfResourcesException(String name) {
+ super(name);
+ }
+ }
+
+ /**
+ * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
+ *
+ * @param texName the OpenGL texture object name (e.g. generated via glGenTextures)
+ *
+ * @throws android.view.Surface.OutOfResourcesException If the SurfaceTexture cannot be created.
+ */
+ public SurfaceTexture(int texName) {
+ this(texName, false);
+ }
+
+ /**
+ * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
+ *
+ * In single buffered mode the application is responsible for serializing access to the image
+ * content buffer. Each time the image content is to be updated, the
+ * {@link #releaseTexImage()} method must be called before the image content producer takes
+ * ownership of the buffer. For example, when producing image content with the NDK
+ * ANativeWindow_lock and ANativeWindow_unlockAndPost functions, {@link #releaseTexImage()}
+ * must be called before each ANativeWindow_lock, or that call will fail. When producing
+ * image content with OpenGL ES, {@link #releaseTexImage()} must be called before the first
+ * OpenGL ES function call each frame.
+ *
+ * @param texName the OpenGL texture object name (e.g. generated via glGenTextures)
+ * @param singleBufferMode whether the SurfaceTexture will be in single buffered mode.
+ *
+ * @throws android.view.Surface.OutOfResourcesException If the SurfaceTexture cannot be created.
+ */
+ public SurfaceTexture(int texName, boolean singleBufferMode) {
+ mCreatorLooper = Looper.myLooper();
+ mIsSingleBuffered = singleBufferMode;
+ nativeInit(false, texName, singleBufferMode, new WeakReference<SurfaceTexture>(this));
+ }
+
+ /**
+ * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
+ *
+ * In single buffered mode the application is responsible for serializing access to the image
+ * content buffer. Each time the image content is to be updated, the
+ * {@link #releaseTexImage()} method must be called before the image content producer takes
+ * ownership of the buffer. For example, when producing image content with the NDK
+ * ANativeWindow_lock and ANativeWindow_unlockAndPost functions, {@link #releaseTexImage()}
+ * must be called before each ANativeWindow_lock, or that call will fail. When producing
+ * image content with OpenGL ES, {@link #releaseTexImage()} must be called before the first
+ * OpenGL ES function call each frame.
+ *
+ * Unlike {@link #SurfaceTexture(int, boolean)}, which takes an OpenGL texture object name,
+ * this constructor creates the SurfaceTexture in detached mode. A texture name must be passed
+ * in using {@link #attachToGLContext} before calling {@link #releaseTexImage()} and producing
+ * image content using OpenGL ES.
+ *
+ * @param singleBufferMode whether the SurfaceTexture will be in single buffered mode.
+ *
+ * @throws android.view.Surface.OutOfResourcesException If the SurfaceTexture cannot be created.
+ */
+ public SurfaceTexture(boolean singleBufferMode) {
+ mCreatorLooper = Looper.myLooper();
+ mIsSingleBuffered = singleBufferMode;
+ nativeInit(true, 0, singleBufferMode, new WeakReference<SurfaceTexture>(this));
+ }
+
+ /**
+ * Register a callback to be invoked when a new image frame becomes available to the
+ * SurfaceTexture.
+ * <p>
+ * The callback may be called on an arbitrary thread, so it is not
+ * safe to call {@link #updateTexImage} without first binding the OpenGL ES context to the
+ * thread invoking the callback.
+ * </p>
+ *
+ * @param listener The listener to use, or null to remove the listener.
+ */
+ public void setOnFrameAvailableListener(@Nullable OnFrameAvailableListener listener) {
+ setOnFrameAvailableListener(listener, null);
+ }
+
+ /**
+ * Register a callback to be invoked when a new image frame becomes available to the
+ * SurfaceTexture.
+ * <p>
+ * If a handler is specified, the callback will be invoked on that handler's thread.
+ * If no handler is specified, then the callback may be called on an arbitrary thread,
+ * so it is not safe to call {@link #updateTexImage} without first binding the OpenGL ES
+ * context to the thread invoking the callback.
+ * </p>
+ *
+ * @param listener The listener to use, or null to remove the listener.
+ * @param handler The handler on which the listener should be invoked, or null
+ * to use an arbitrary thread.
+ */
+ public void setOnFrameAvailableListener(@Nullable final OnFrameAvailableListener listener,
+ @Nullable Handler handler) {
+ if (listener != null) {
+ // Although we claim the thread is arbitrary, earlier implementation would
+ // prefer to send the callback on the creating looper or the main looper
+ // so we preserve this behavior here.
+ Looper looper = handler != null ? handler.getLooper() :
+ mCreatorLooper != null ? mCreatorLooper : Looper.getMainLooper();
+ mOnFrameAvailableHandler = new Handler(looper, null, true /*async*/) {
+ @Override
+ public void handleMessage(Message msg) {
+ listener.onFrameAvailable(SurfaceTexture.this);
+ }
+ };
+ } else {
+ mOnFrameAvailableHandler = null;
+ }
+ }
+
+ /**
+ * Set the default size of the image buffers. The image producer may override the buffer size,
+ * in which case the producer-set buffer size will be used, not the default size set by this
+ * method. Both video and camera based image producers do override the size. This method may
+ * be used to set the image size when producing images with {@link android.graphics.Canvas} (via
+ * {@link android.view.Surface#lockCanvas}), or OpenGL ES (via an EGLSurface).
+ *
+ * The new default buffer size will take effect the next time the image producer requests a
+ * buffer to fill. For {@link android.graphics.Canvas} this will be the next time {@link
+ * android.view.Surface#lockCanvas} is called. For OpenGL ES, the EGLSurface should be
+ * destroyed (via eglDestroySurface), made not-current (via eglMakeCurrent), and then recreated
+ * (via eglCreateWindowSurface) to ensure that the new default size has taken effect.
+ *
+ * The width and height parameters must be no greater than the minimum of
+ * GL_MAX_VIEWPORT_DIMS and GL_MAX_TEXTURE_SIZE (see
+ * {@link javax.microedition.khronos.opengles.GL10#glGetIntegerv glGetIntegerv}).
+ * An error due to invalid dimensions might not be reported until
+ * updateTexImage() is called.
+ */
+ public void setDefaultBufferSize(int width, int height) {
+ nativeSetDefaultBufferSize(width, height);
+ }
+
+ /**
+ * Update the texture image to the most recent frame from the image stream. This may only be
+ * called while the OpenGL ES context that owns the texture is current on the calling thread.
+ * It will implicitly bind its texture to the GL_TEXTURE_EXTERNAL_OES texture target.
+ */
+ public void updateTexImage() {
+ nativeUpdateTexImage();
+ }
+
+ /**
+ * Releases the the texture content. This is needed in single buffered mode to allow the image
+ * content producer to take ownership of the image buffer.
+ * For more information see {@link #SurfaceTexture(int, boolean)}.
+ */
+ public void releaseTexImage() {
+ nativeReleaseTexImage();
+ }
+
+ /**
+ * Detach the SurfaceTexture from the OpenGL ES context that owns the OpenGL ES texture object.
+ * This call must be made with the OpenGL ES context current on the calling thread. The OpenGL
+ * ES texture object will be deleted as a result of this call. After calling this method all
+ * calls to {@link #updateTexImage} will throw an {@link java.lang.IllegalStateException} until
+ * a successful call to {@link #attachToGLContext} is made.
+ *
+ * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES
+ * contexts. Note, however, that the image contents are only accessible from one OpenGL ES
+ * context at a time.
+ */
+ public void detachFromGLContext() {
+ int err = nativeDetachFromGLContext();
+ if (err != 0) {
+ throw new RuntimeException("Error during detachFromGLContext (see logcat for details)");
+ }
+ }
+
+ /**
+ * Attach the SurfaceTexture to the OpenGL ES context that is current on the calling thread. A
+ * new OpenGL ES texture object is created and populated with the SurfaceTexture image frame
+ * that was current at the time of the last call to {@link #detachFromGLContext}. This new
+ * texture is bound to the GL_TEXTURE_EXTERNAL_OES texture target.
+ *
+ * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES
+ * contexts. Note, however, that the image contents are only accessible from one OpenGL ES
+ * context at a time.
+ *
+ * @param texName The name of the OpenGL ES texture that will be created. This texture name
+ * must be unusued in the OpenGL ES context that is current on the calling thread.
+ */
+ public void attachToGLContext(int texName) {
+ int err = nativeAttachToGLContext(texName);
+ if (err != 0) {
+ throw new RuntimeException("Error during attachToGLContext (see logcat for details)");
+ }
+ }
+
+ /**
+ * Retrieve the 4x4 texture coordinate transform matrix associated with the texture image set by
+ * the most recent call to updateTexImage.
+ *
+ * This transform matrix maps 2D homogeneous texture coordinates of the form (s, t, 0, 1) with s
+ * and t in the inclusive range [0, 1] to the texture coordinate that should be used to sample
+ * that location from the texture. Sampling the texture outside of the range of this transform
+ * is undefined.
+ *
+ * The matrix is stored in column-major order so that it may be passed directly to OpenGL ES via
+ * the glLoadMatrixf or glUniformMatrix4fv functions.
+ *
+ * @param mtx the array into which the 4x4 matrix will be stored. The array must have exactly
+ * 16 elements.
+ */
+ public void getTransformMatrix(float[] mtx) {
+ // Note we intentionally don't check mtx for null, so this will result in a
+ // NullPointerException. But it's safe because it happens before the call to native.
+ if (mtx.length != 16) {
+ throw new IllegalArgumentException();
+ }
+ nativeGetTransformMatrix(mtx);
+ }
+
+ /**
+ * Retrieve the timestamp associated with the texture image set by the most recent call to
+ * updateTexImage.
+ *
+ * This timestamp is in nanoseconds, and is normally monotonically increasing. The timestamp
+ * should be unaffected by time-of-day adjustments, and for a camera should be strictly
+ * monotonic but for a MediaPlayer may be reset when the position is set. The
+ * specific meaning and zero point of the timestamp depends on the source providing images to
+ * the SurfaceTexture. Unless otherwise specified by the image source, timestamps cannot
+ * generally be compared across SurfaceTexture instances, or across multiple program
+ * invocations. It is mostly useful for determining time offsets between subsequent frames.
+ */
+
+ public long getTimestamp() {
+ return nativeGetTimestamp();
+ }
+
+ /**
+ * release() frees all the buffers and puts the SurfaceTexture into the
+ * 'abandoned' state. Once put in this state the SurfaceTexture can never
+ * leave it. When in the 'abandoned' state, all methods of the
+ * IGraphicBufferProducer interface will fail with the NO_INIT error.
+ *
+ * Note that while calling this method causes all the buffers to be freed
+ * from the perspective of the the SurfaceTexture, if there are additional
+ * references on the buffers (e.g. if a buffer is referenced by a client or
+ * by OpenGL ES as a texture) then those buffer will remain allocated.
+ *
+ * Always call this method when you are done with SurfaceTexture. Failing
+ * to do so may delay resource deallocation for a significant amount of
+ * time.
+ *
+ * @see #isReleased()
+ */
+ public void release() {
+ nativeRelease();
+ }
+
+ /**
+ * Returns true if the SurfaceTexture was released.
+ *
+ * @see #release()
+ */
+ public boolean isReleased() {
+ return nativeIsReleased();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ nativeFinalize();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * This method is invoked from native code only.
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ private static void postEventFromNative(WeakReference<SurfaceTexture> weakSelf) {
+ SurfaceTexture st = weakSelf.get();
+ if (st != null) {
+ Handler handler = st.mOnFrameAvailableHandler;
+ if (handler != null) {
+ handler.sendEmptyMessage(0);
+ }
+ }
+ }
+
+ /**
+ * Returns true if the SurfaceTexture is single-buffered
+ * @hide
+ */
+ public boolean isSingleBuffered() {
+ return mIsSingleBuffered;
+ }
+
+ private native void nativeInit(boolean isDetached, int texName,
+ boolean singleBufferMode, WeakReference<SurfaceTexture> weakSelf)
+ throws Surface.OutOfResourcesException;
+ private native void nativeFinalize();
+ private native void nativeGetTransformMatrix(float[] mtx);
+ private native long nativeGetTimestamp();
+ private native void nativeSetDefaultBufferSize(int width, int height);
+ private native void nativeUpdateTexImage();
+ private native void nativeReleaseTexImage();
+ private native int nativeDetachFromGLContext();
+ private native int nativeAttachToGLContext(int texName);
+ private native void nativeRelease();
+ private native boolean nativeIsReleased();
+}
diff --git a/android/graphics/SweepGradient.java b/android/graphics/SweepGradient.java
new file mode 100644
index 0000000..b6b80b4
--- /dev/null
+++ b/android/graphics/SweepGradient.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+public class SweepGradient extends Shader {
+
+ private static final int TYPE_COLORS_AND_POSITIONS = 1;
+ private static final int TYPE_COLOR_START_AND_COLOR_END = 2;
+
+ /**
+ * Type of the LinearGradient: can be either TYPE_COLORS_AND_POSITIONS or
+ * TYPE_COLOR_START_AND_COLOR_END.
+ */
+ private int mType;
+
+ private float mCx;
+ private float mCy;
+ private int[] mColors;
+ private float[] mPositions;
+ private int mColor0;
+ private int mColor1;
+
+ /**
+ * A Shader that draws a sweep gradient around a center point.
+ *
+ * @param cx The x-coordinate of the center
+ * @param cy The y-coordinate of the center
+ * @param colors The colors to be distributed between around the center.
+ * There must be at least 2 colors in the array.
+ * @param positions May be NULL. The relative position of
+ * each corresponding color in the colors array, beginning
+ * with 0 and ending with 1.0. If the values are not
+ * monotonic, the drawing may produce unexpected results.
+ * If positions is NULL, then the colors are automatically
+ * spaced evenly.
+ */
+ public SweepGradient(float cx, float cy,
+ @NonNull @ColorInt int colors[], @Nullable float positions[]) {
+ if (colors.length < 2) {
+ throw new IllegalArgumentException("needs >= 2 number of colors");
+ }
+ if (positions != null && colors.length != positions.length) {
+ throw new IllegalArgumentException(
+ "color and position arrays must be of equal length");
+ }
+ mType = TYPE_COLORS_AND_POSITIONS;
+ mCx = cx;
+ mCy = cy;
+ mColors = colors.clone();
+ mPositions = positions != null ? positions.clone() : null;
+ }
+
+ /**
+ * A Shader that draws a sweep gradient around a center point.
+ *
+ * @param cx The x-coordinate of the center
+ * @param cy The y-coordinate of the center
+ * @param color0 The color to use at the start of the sweep
+ * @param color1 The color to use at the end of the sweep
+ */
+ public SweepGradient(float cx, float cy, @ColorInt int color0, @ColorInt int color1) {
+ mType = TYPE_COLOR_START_AND_COLOR_END;
+ mCx = cx;
+ mCy = cy;
+ mColor0 = color0;
+ mColor1 = color1;
+ mColors = null;
+ mPositions = null;
+ }
+
+ @Override
+ long createNativeInstance(long nativeMatrix) {
+ if (mType == TYPE_COLORS_AND_POSITIONS) {
+ return nativeCreate1(nativeMatrix, mCx, mCy, mColors, mPositions);
+ } else { // TYPE_COLOR_START_AND_COLOR_END
+ return nativeCreate2(nativeMatrix, mCx, mCy, mColor0, mColor1);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected Shader copy() {
+ final SweepGradient copy;
+ if (mType == TYPE_COLORS_AND_POSITIONS) {
+ copy = new SweepGradient(mCx, mCy, mColors.clone(),
+ mPositions != null ? mPositions.clone() : null);
+ } else { // TYPE_COLOR_START_AND_COLOR_END
+ copy = new SweepGradient(mCx, mCy, mColor0, mColor1);
+ }
+ copyLocalMatrix(copy);
+ return copy;
+ }
+
+ private static native long nativeCreate1(long matrix, float x, float y,
+ int colors[], float positions[]);
+ private static native long nativeCreate2(long matrix, float x, float y,
+ int color0, int color1);
+}
+
diff --git a/android/graphics/SweepGradient_Delegate.java b/android/graphics/SweepGradient_Delegate.java
new file mode 100644
index 0000000..6e8aca3
--- /dev/null
+++ b/android/graphics/SweepGradient_Delegate.java
@@ -0,0 +1,214 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import java.awt.image.DataBufferInt;
+import java.awt.image.Raster;
+import java.awt.image.SampleModel;
+
+/**
+ * Delegate implementing the native methods of android.graphics.SweepGradient
+ *
+ * Through the layoutlib_create tool, the original native methods of SweepGradient have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between
+ * it and the original SweepGradient class.
+ *
+ * Because this extends {@link Shader_Delegate}, there's no need to use a {@link DelegateManager},
+ * as all the Shader classes will be added to the manager owned by {@link Shader_Delegate}.
+ *
+ * @see Shader_Delegate
+ *
+ */
+public class SweepGradient_Delegate extends Gradient_Delegate {
+
+ // ---- delegate data ----
+ private java.awt.Paint mJavaPaint;
+
+ // ---- Public Helper methods ----
+
+ @Override
+ public java.awt.Paint getJavaPaint() {
+ return mJavaPaint;
+ }
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate1(long matrix, float x, float y, int colors[], float
+ positions[]) {
+ SweepGradient_Delegate newDelegate = new SweepGradient_Delegate(matrix, x, y, colors,
+ positions);
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreate2(long matrix, float x, float y, int color0, int color1) {
+ return nativeCreate1(matrix, x, y, new int[] { color0, color1 },
+ null /*positions*/);
+ }
+
+ // ---- Private delegate/helper methods ----
+
+ /**
+ * A subclass of Shader that draws a sweep gradient around a center point.
+ *
+ * @param nativeMatrix reference to the shader's native transformation matrix
+ * @param cx The x-coordinate of the center
+ * @param cy The y-coordinate of the center
+ * @param colors The colors to be distributed between around the center.
+ * There must be at least 2 colors in the array.
+ * @param positions May be NULL. The relative position of
+ * each corresponding color in the colors array, beginning
+ * with 0 and ending with 1.0. If the values are not
+ * monotonic, the drawing may produce unexpected results.
+ * If positions is NULL, then the colors are automatically
+ * spaced evenly.
+ */
+ private SweepGradient_Delegate(long nativeMatrix, float cx, float cy,
+ int colors[], float positions[]) {
+ super(nativeMatrix, colors, positions);
+ mJavaPaint = new SweepGradientPaint(cx, cy, mColors, mPositions);
+ }
+
+ private class SweepGradientPaint extends GradientPaint {
+
+ private final float mCx;
+ private final float mCy;
+
+ public SweepGradientPaint(float cx, float cy, int[] colors,
+ float[] positions) {
+ super(colors, positions, null /*tileMode*/);
+ mCx = cx;
+ mCy = cy;
+ }
+
+ @Override
+ public java.awt.PaintContext createContext(
+ java.awt.image.ColorModel colorModel,
+ java.awt.Rectangle deviceBounds,
+ java.awt.geom.Rectangle2D userBounds,
+ java.awt.geom.AffineTransform xform,
+ java.awt.RenderingHints hints) {
+ precomputeGradientColors();
+
+ java.awt.geom.AffineTransform canvasMatrix;
+ try {
+ canvasMatrix = xform.createInverse();
+ } catch (java.awt.geom.NoninvertibleTransformException e) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE,
+ "Unable to inverse matrix in SweepGradient", e, null /*data*/);
+ canvasMatrix = new java.awt.geom.AffineTransform();
+ }
+
+ java.awt.geom.AffineTransform localMatrix = getLocalMatrix();
+ try {
+ localMatrix = localMatrix.createInverse();
+ } catch (java.awt.geom.NoninvertibleTransformException e) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE,
+ "Unable to inverse matrix in SweepGradient", e, null /*data*/);
+ localMatrix = new java.awt.geom.AffineTransform();
+ }
+
+ return new SweepGradientPaintContext(canvasMatrix, localMatrix, colorModel);
+ }
+
+ private class SweepGradientPaintContext implements java.awt.PaintContext {
+
+ private final java.awt.geom.AffineTransform mCanvasMatrix;
+ private final java.awt.geom.AffineTransform mLocalMatrix;
+ private final java.awt.image.ColorModel mColorModel;
+
+ public SweepGradientPaintContext(
+ java.awt.geom.AffineTransform canvasMatrix,
+ java.awt.geom.AffineTransform localMatrix,
+ java.awt.image.ColorModel colorModel) {
+ mCanvasMatrix = canvasMatrix;
+ mLocalMatrix = localMatrix;
+ mColorModel = colorModel;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public java.awt.image.ColorModel getColorModel() {
+ return mColorModel;
+ }
+
+ @Override
+ public java.awt.image.Raster getRaster(int x, int y, int w, int h) {
+
+ int[] data = new int[w*h];
+
+ // compute angle from each point to the center, and figure out the distance from
+ // it.
+ int index = 0;
+ float[] pt1 = new float[2];
+ float[] pt2 = new float[2];
+ for (int iy = 0 ; iy < h ; iy++) {
+ for (int ix = 0 ; ix < w ; ix++) {
+ // handle the canvas transform
+ pt1[0] = x + ix;
+ pt1[1] = y + iy;
+ mCanvasMatrix.transform(pt1, 0, pt2, 0, 1);
+
+ // handle the local matrix
+ pt1[0] = pt2[0] - mCx;
+ pt1[1] = pt2[1] - mCy;
+ mLocalMatrix.transform(pt1, 0, pt2, 0, 1);
+
+ float dx = pt2[0];
+ float dy = pt2[1];
+
+ float angle;
+ if (dx == 0) {
+ angle = (float) (dy < 0 ? 3 * Math.PI / 2 : Math.PI / 2);
+ } else if (dy == 0) {
+ angle = (float) (dx < 0 ? Math.PI : 0);
+ } else {
+ angle = (float) Math.atan(dy / dx);
+ if (dx > 0) {
+ if (dy < 0) {
+ angle += Math.PI * 2;
+ }
+ } else {
+ angle += Math.PI;
+ }
+ }
+
+ // convert to 0-1. value and get color
+ data[index++] = getGradientColor((float) (angle / (2 * Math.PI)));
+ }
+ }
+
+ DataBufferInt dataBuffer = new DataBufferInt(data, data.length);
+ SampleModel colorModel = mColorModel.createCompatibleSampleModel(w, h);
+ return Raster.createWritableRaster(colorModel, dataBuffer, null);
+ }
+
+ }
+ }
+}
diff --git a/android/graphics/TableMaskFilter.java b/android/graphics/TableMaskFilter.java
new file mode 100644
index 0000000..d0c1438
--- /dev/null
+++ b/android/graphics/TableMaskFilter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+/**
+ * @hide
+ */
+public class TableMaskFilter extends MaskFilter {
+
+ public TableMaskFilter(byte[] table) {
+ if (table.length < 256) {
+ throw new RuntimeException("table.length must be >= 256");
+ }
+ native_instance = nativeNewTable(table);
+ }
+
+ private TableMaskFilter(long ni) {
+ native_instance = ni;
+ }
+
+ public static TableMaskFilter CreateClipTable(int min, int max) {
+ return new TableMaskFilter(nativeNewClip(min, max));
+ }
+
+ public static TableMaskFilter CreateGammaTable(float gamma) {
+ return new TableMaskFilter(nativeNewGamma(gamma));
+ }
+
+ private static native long nativeNewTable(byte[] table);
+ private static native long nativeNewClip(int min, int max);
+ private static native long nativeNewGamma(float gamma);
+}
diff --git a/android/graphics/TemporaryBuffer.java b/android/graphics/TemporaryBuffer.java
new file mode 100644
index 0000000..36a2275
--- /dev/null
+++ b/android/graphics/TemporaryBuffer.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import com.android.internal.util.ArrayUtils;
+
+/**
+ * @hide
+ */
+public class TemporaryBuffer {
+ public static char[] obtain(int len) {
+ char[] buf;
+
+ synchronized (TemporaryBuffer.class) {
+ buf = sTemp;
+ sTemp = null;
+ }
+
+ if (buf == null || buf.length < len) {
+ buf = ArrayUtils.newUnpaddedCharArray(len);
+ }
+
+ return buf;
+ }
+
+ public static void recycle(char[] temp) {
+ if (temp.length > 1000) return;
+
+ synchronized (TemporaryBuffer.class) {
+ sTemp = temp;
+ }
+ }
+
+ private static char[] sTemp = null;
+}
diff --git a/android/graphics/Typeface.java b/android/graphics/Typeface.java
new file mode 100644
index 0000000..cfc389f
--- /dev/null
+++ b/android/graphics/Typeface.java
@@ -0,0 +1,1204 @@
+/*
+ * Copyright (C) 2006 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 android.graphics;
+
+import static android.content.res.FontResourcesParser.ProviderResourceEntry;
+import static android.content.res.FontResourcesParser.FontFileResourceEntry;
+import static android.content.res.FontResourcesParser.FontFamilyFilesResourceEntry;
+import static android.content.res.FontResourcesParser.FamilyResourceEntry;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.AssetManager;
+import android.graphics.FontListParser;
+import android.graphics.fonts.FontVariationAxis;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.os.ResultReceiver;
+import android.provider.FontRequest;
+import android.provider.FontsContract;
+import android.text.FontConfig;
+import android.util.ArrayMap;
+import android.util.Base64;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.LruCache;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * The Typeface class specifies the typeface and intrinsic style of a font.
+ * This is used in the paint, along with optionally Paint settings like
+ * textSize, textSkewX, textScaleX to specify
+ * how text appears when drawn (and measured).
+ */
+public class Typeface {
+
+ private static String TAG = "Typeface";
+
+ /** The default NORMAL typeface object */
+ public static final Typeface DEFAULT;
+ /**
+ * The default BOLD typeface object. Note: this may be not actually be
+ * bold, depending on what fonts are installed. Call getStyle() to know
+ * for sure.
+ */
+ public static final Typeface DEFAULT_BOLD;
+ /** The NORMAL style of the default sans serif typeface. */
+ public static final Typeface SANS_SERIF;
+ /** The NORMAL style of the default serif typeface. */
+ public static final Typeface SERIF;
+ /** The NORMAL style of the default monospace typeface. */
+ public static final Typeface MONOSPACE;
+
+ static Typeface[] sDefaults;
+
+ /**
+ * Cache for Typeface objects for style variant. Currently max size is 3.
+ */
+ @GuardedBy("sStyledCacheLock")
+ private static final LongSparseArray<SparseArray<Typeface>> sStyledTypefaceCache =
+ new LongSparseArray<>(3);
+ private static final Object sStyledCacheLock = new Object();
+
+ /**
+ * Cache for Typeface objects for weight variant. Currently max size is 3.
+ */
+ @GuardedBy("sWeightCacheLock")
+ private static final LongSparseArray<SparseArray<Typeface>> sWeightTypefaceCache =
+ new LongSparseArray<>(3);
+ private static final Object sWeightCacheLock = new Object();
+
+ /**
+ * Cache for Typeface objects dynamically loaded from assets. Currently max size is 16.
+ */
+ @GuardedBy("sDynamicCacheLock")
+ private static final LruCache<String, Typeface> sDynamicTypefaceCache = new LruCache<>(16);
+ private static final Object sDynamicCacheLock = new Object();
+
+ static Typeface sDefaultTypeface;
+ static final Map<String, Typeface> sSystemFontMap;
+ static final Map<String, FontFamily[]> sSystemFallbackMap;
+
+ /**
+ * @hide
+ */
+ public long native_instance;
+
+ // Style
+ public static final int NORMAL = 0;
+ public static final int BOLD = 1;
+ public static final int ITALIC = 2;
+ public static final int BOLD_ITALIC = 3;
+ /** @hide */ public static final int STYLE_MASK = 0x03;
+
+ private int mStyle = 0;
+ private int mWeight = 0;
+
+ // Value for weight and italic. Indicates the value is resolved by font metadata.
+ // Must be the same as the C++ constant in core/jni/android/graphics/FontFamily.cpp
+ /** @hide */
+ public static final int RESOLVE_BY_FONT_TABLE = -1;
+ private static final String DEFAULT_FAMILY = "sans-serif";
+
+ // Style value for building typeface.
+ private static final int STYLE_NORMAL = 0;
+ private static final int STYLE_ITALIC = 1;
+
+ private int[] mSupportedAxes;
+ private static final int[] EMPTY_AXES = {};
+
+ private static void setDefault(Typeface t) {
+ sDefaultTypeface = t;
+ nativeSetDefault(t.native_instance);
+ }
+
+ // TODO: Make this public API. (b/64852739)
+ /** @hide */
+ @VisibleForTesting
+ public int getWeight() {
+ return mWeight;
+ }
+
+ /** Returns the typeface's intrinsic style attributes */
+ public int getStyle() {
+ return mStyle;
+ }
+
+ /** Returns true if getStyle() has the BOLD bit set. */
+ public final boolean isBold() {
+ return (mStyle & BOLD) != 0;
+ }
+
+ /** Returns true if getStyle() has the ITALIC bit set. */
+ public final boolean isItalic() {
+ return (mStyle & ITALIC) != 0;
+ }
+
+ /**
+ * @hide
+ * Used by Resources to load a font resource of type font file.
+ */
+ @Nullable
+ public static Typeface createFromResources(AssetManager mgr, String path, int cookie) {
+ synchronized (sDynamicCacheLock) {
+ final String key = Builder.createAssetUid(
+ mgr, path, 0 /* ttcIndex */, null /* axes */,
+ RESOLVE_BY_FONT_TABLE /* weight */, RESOLVE_BY_FONT_TABLE /* italic */,
+ DEFAULT_FAMILY);
+ Typeface typeface = sDynamicTypefaceCache.get(key);
+ if (typeface != null) return typeface;
+
+ FontFamily fontFamily = new FontFamily();
+ // TODO: introduce ttc index and variation settings to resource type font.
+ if (fontFamily.addFontFromAssetManager(mgr, path, cookie, false /* isAsset */,
+ 0 /* ttcIndex */, RESOLVE_BY_FONT_TABLE /* weight */,
+ RESOLVE_BY_FONT_TABLE /* italic */, null /* axes */)) {
+ if (!fontFamily.freeze()) {
+ return null;
+ }
+ FontFamily[] families = {fontFamily};
+ typeface = createFromFamiliesWithDefault(families, DEFAULT_FAMILY,
+ RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
+ sDynamicTypefaceCache.put(key, typeface);
+ return typeface;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ * Used by Resources to load a font resource of type xml.
+ */
+ @Nullable
+ public static Typeface createFromResources(
+ FamilyResourceEntry entry, AssetManager mgr, String path) {
+ if (entry instanceof ProviderResourceEntry) {
+ final ProviderResourceEntry providerEntry = (ProviderResourceEntry) entry;
+ // Downloadable font
+ List<List<String>> givenCerts = providerEntry.getCerts();
+ List<List<byte[]>> certs = new ArrayList<>();
+ if (givenCerts != null) {
+ for (int i = 0; i < givenCerts.size(); i++) {
+ List<String> certSet = givenCerts.get(i);
+ List<byte[]> byteArraySet = new ArrayList<>();
+ for (int j = 0; j < certSet.size(); j++) {
+ byteArraySet.add(Base64.decode(certSet.get(j), Base64.DEFAULT));
+ }
+ certs.add(byteArraySet);
+ }
+ }
+ // Downloaded font and it wasn't cached, request it again and return a
+ // default font instead (nothing we can do now).
+ FontRequest request = new FontRequest(providerEntry.getAuthority(),
+ providerEntry.getPackage(), providerEntry.getQuery(), certs);
+ Typeface typeface = FontsContract.getFontSync(request);
+ return typeface == null ? DEFAULT : typeface;
+ }
+
+ Typeface typeface = findFromCache(mgr, path);
+ if (typeface != null) return typeface;
+
+ // family is FontFamilyFilesResourceEntry
+ final FontFamilyFilesResourceEntry filesEntry = (FontFamilyFilesResourceEntry) entry;
+
+ FontFamily fontFamily = new FontFamily();
+ for (final FontFileResourceEntry fontFile : filesEntry.getEntries()) {
+ // TODO: Add ttc and variation font support. (b/37853920)
+ if (!fontFamily.addFontFromAssetManager(mgr, fontFile.getFileName(),
+ 0 /* resourceCookie */, false /* isAsset */, 0 /* ttcIndex */,
+ fontFile.getWeight(), fontFile.getItalic(), null /* axes */)) {
+ return null;
+ }
+ }
+ if (!fontFamily.freeze()) {
+ return null;
+ }
+ FontFamily[] familyChain = { fontFamily };
+ typeface = createFromFamiliesWithDefault(familyChain, DEFAULT_FAMILY,
+ RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
+ synchronized (sDynamicCacheLock) {
+ final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */,
+ null /* axes */, RESOLVE_BY_FONT_TABLE /* weight */,
+ RESOLVE_BY_FONT_TABLE /* italic */, DEFAULT_FAMILY);
+ sDynamicTypefaceCache.put(key, typeface);
+ }
+ return typeface;
+ }
+
+ /**
+ * Used by resources for cached loading if the font is available.
+ * @hide
+ */
+ public static Typeface findFromCache(AssetManager mgr, String path) {
+ synchronized (sDynamicCacheLock) {
+ final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, null /* axes */,
+ RESOLVE_BY_FONT_TABLE /* weight */, RESOLVE_BY_FONT_TABLE /* italic */,
+ DEFAULT_FAMILY);
+ Typeface typeface = sDynamicTypefaceCache.get(key);
+ if (typeface != null) {
+ return typeface;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * A builder class for creating new Typeface instance.
+ *
+ * <p>
+ * Examples,
+ * 1) Create Typeface from ttf file.
+ * <pre>
+ * <code>
+ * Typeface.Builder buidler = new Typeface.Builder("your_font_file.ttf");
+ * Typeface typeface = builder.build();
+ * </code>
+ * </pre>
+ *
+ * 2) Create Typeface from ttc file in assets directory.
+ * <pre>
+ * <code>
+ * Typeface.Builder buidler = new Typeface.Builder(getAssets(), "your_font_file.ttc");
+ * builder.setTtcIndex(2); // Set index of font collection.
+ * Typeface typeface = builder.build();
+ * </code>
+ * </pre>
+ *
+ * 3) Create Typeface with variation settings.
+ * <pre>
+ * <code>
+ * Typeface.Builder buidler = new Typeface.Builder("your_font_file.ttf");
+ * builder.setFontVariationSettings("'wght' 700, 'slnt' 20, 'ital' 1");
+ * builder.setWeight(700); // Tell the system that this is a bold font.
+ * builder.setItalic(true); // Tell the system that this is an italic style font.
+ * Typeface typeface = builder.build();
+ * </code>
+ * </pre>
+ * </p>
+ */
+ public static final class Builder {
+ /** @hide */
+ public static final int NORMAL_WEIGHT = 400;
+ /** @hide */
+ public static final int BOLD_WEIGHT = 700;
+
+ private int mTtcIndex;
+ private FontVariationAxis[] mAxes;
+
+ private AssetManager mAssetManager;
+ private String mPath;
+ private FileDescriptor mFd;
+
+ private FontsContract.FontInfo[] mFonts;
+ private Map<Uri, ByteBuffer> mFontBuffers;
+
+ private String mFallbackFamilyName;
+
+ private int mWeight = RESOLVE_BY_FONT_TABLE;
+ private int mItalic = RESOLVE_BY_FONT_TABLE;
+
+ /**
+ * Constructs a builder with a file path.
+ *
+ * @param path The file object refers to the font file.
+ */
+ public Builder(@NonNull File path) {
+ mPath = path.getAbsolutePath();
+ }
+
+ /**
+ * Constructs a builder with a file descriptor.
+ *
+ * Caller is responsible for closing the passed file descriptor after {@link #build} is
+ * called.
+ *
+ * @param fd The file descriptor. The passed fd must be mmap-able.
+ */
+ public Builder(@NonNull FileDescriptor fd) {
+ mFd = fd;
+ }
+
+ /**
+ * Constructs a builder with a file path.
+ *
+ * @param path The full path to the font file.
+ */
+ public Builder(@NonNull String path) {
+ mPath = path;
+ }
+
+ /**
+ * Constructs a builder from an asset manager and a file path in an asset directory.
+ *
+ * @param assetManager The application's asset manager
+ * @param path The file name of the font data in the asset directory
+ */
+ public Builder(@NonNull AssetManager assetManager, @NonNull String path) {
+ mAssetManager = Preconditions.checkNotNull(assetManager);
+ mPath = Preconditions.checkStringNotEmpty(path);
+ }
+
+ /**
+ * Constracts a builder from an array of FontsContract.FontInfo.
+ *
+ * Since {@link FontsContract.FontInfo} holds information about TTC indices and
+ * variation settings, there is no need to call {@link #setTtcIndex} or
+ * {@link #setFontVariationSettings}. Similary, {@link FontsContract.FontInfo} holds
+ * weight and italic information, so {@link #setWeight} and {@link #setItalic} are used
+ * for style matching during font selection.
+ *
+ * @param results The array of {@link FontsContract.FontInfo}
+ * @param buffers The mapping from URI to buffers to be used during building.
+ * @hide
+ */
+ public Builder(@NonNull FontsContract.FontInfo[] fonts,
+ @NonNull Map<Uri, ByteBuffer> buffers) {
+ mFonts = fonts;
+ mFontBuffers = buffers;
+ }
+
+ /**
+ * Sets weight of the font.
+ *
+ * Tells the system the weight of the given font. If not provided, the system will resolve
+ * the weight value by reading font tables.
+ * @param weight a weight value.
+ */
+ public Builder setWeight(@IntRange(from = 1, to = 1000) int weight) {
+ mWeight = weight;
+ return this;
+ }
+
+ /**
+ * Sets italic information of the font.
+ *
+ * Tells the system the style of the given font. If not provided, the system will resolve
+ * the style by reading font tables.
+ * @param italic {@code true} if the font is italic. Otherwise {@code false}.
+ */
+ public Builder setItalic(boolean italic) {
+ mItalic = italic ? STYLE_ITALIC : STYLE_NORMAL;
+ return this;
+ }
+
+ /**
+ * Sets an index of the font collection.
+ *
+ * Can not be used for Typeface source. build() method will return null for invalid index.
+ * @param ttcIndex An index of the font collection. If the font source is not font
+ * collection, do not call this method or specify 0.
+ */
+ public Builder setTtcIndex(@IntRange(from = 0) int ttcIndex) {
+ if (mFonts != null) {
+ throw new IllegalArgumentException(
+ "TTC index can not be specified for FontResult source.");
+ }
+ mTtcIndex = ttcIndex;
+ return this;
+ }
+
+ /**
+ * Sets a font variation settings.
+ *
+ * @param variationSettings See {@link android.widget.TextView#setFontVariationSettings}.
+ * @throws IllegalArgumentException If given string is not a valid font variation settings
+ * format.
+ */
+ public Builder setFontVariationSettings(@Nullable String variationSettings) {
+ if (mFonts != null) {
+ throw new IllegalArgumentException(
+ "Font variation settings can not be specified for FontResult source.");
+ }
+ if (mAxes != null) {
+ throw new IllegalStateException("Font variation settings are already set.");
+ }
+ mAxes = FontVariationAxis.fromFontVariationSettings(variationSettings);
+ return this;
+ }
+
+ /**
+ * Sets a font variation settings.
+ *
+ * @param axes An array of font variation axis tag-value pairs.
+ */
+ public Builder setFontVariationSettings(@Nullable FontVariationAxis[] axes) {
+ if (mFonts != null) {
+ throw new IllegalArgumentException(
+ "Font variation settings can not be specified for FontResult source.");
+ }
+ if (mAxes != null) {
+ throw new IllegalStateException("Font variation settings are already set.");
+ }
+ mAxes = axes;
+ return this;
+ }
+
+ /**
+ * Sets a fallback family name.
+ *
+ * By specifying a fallback family name, a fallback Typeface will be returned if the
+ * {@link #build} method fails to create a Typeface from the provided font. The fallback
+ * family will be resolved with the provided weight and italic information specified by
+ * {@link #setWeight} and {@link #setItalic}.
+ *
+ * If {@link #setWeight} is not called, the fallback family keeps the default weight.
+ * Similary, if {@link #setItalic} is not called, the fallback family keeps the default
+ * italic information. For example, calling {@code builder.setFallback("sans-serif-light")}
+ * is equivalent to calling {@code builder.setFallback("sans-serif").setWeight(300)} in
+ * terms of fallback. The default weight and italic information are overridden by calling
+ * {@link #setWeight} and {@link #setItalic}. For example, if a Typeface is constructed
+ * using {@code builder.setFallback("sans-serif-light").setWeight(700)}, the fallback text
+ * will render as sans serif bold.
+ *
+ * @param familyName A family name to be used for fallback if the provided font can not be
+ * used. By passing {@code null}, build() returns {@code null}.
+ * If {@link #setFallback} is not called on the builder, {@code null}
+ * is assumed.
+ */
+ public Builder setFallback(@Nullable String familyName) {
+ mFallbackFamilyName = familyName;
+ return this;
+ }
+
+ /**
+ * Creates a unique id for a given AssetManager and asset path.
+ *
+ * @param mgr AssetManager instance
+ * @param path The path for the asset.
+ * @param ttcIndex The TTC index for the font.
+ * @param axes The font variation settings.
+ * @return Unique id for a given AssetManager and asset path.
+ */
+ private static String createAssetUid(final AssetManager mgr, String path, int ttcIndex,
+ @Nullable FontVariationAxis[] axes, int weight, int italic, String fallback) {
+ final SparseArray<String> pkgs = mgr.getAssignedPackageIdentifiers();
+ final StringBuilder builder = new StringBuilder();
+ final int size = pkgs.size();
+ for (int i = 0; i < size; i++) {
+ builder.append(pkgs.valueAt(i));
+ builder.append("-");
+ }
+ builder.append(path);
+ builder.append("-");
+ builder.append(Integer.toString(ttcIndex));
+ builder.append("-");
+ builder.append(Integer.toString(weight));
+ builder.append("-");
+ builder.append(Integer.toString(italic));
+ // Family name may contain hyphen. Use double hyphen for avoiding key conflicts before
+ // and after appending falblack name.
+ builder.append("--");
+ builder.append(fallback);
+ builder.append("--");
+ if (axes != null) {
+ for (FontVariationAxis axis : axes) {
+ builder.append(axis.getTag());
+ builder.append("-");
+ builder.append(Float.toString(axis.getStyleValue()));
+ }
+ }
+ return builder.toString();
+ }
+
+ private Typeface resolveFallbackTypeface() {
+ if (mFallbackFamilyName == null) {
+ return null;
+ }
+
+ Typeface base = sSystemFontMap.get(mFallbackFamilyName);
+ if (base == null) {
+ base = sDefaultTypeface;
+ }
+
+ if (mWeight == RESOLVE_BY_FONT_TABLE && mItalic == RESOLVE_BY_FONT_TABLE) {
+ return base;
+ }
+
+ final int weight = (mWeight == RESOLVE_BY_FONT_TABLE) ? base.mWeight : mWeight;
+ final boolean italic =
+ (mItalic == RESOLVE_BY_FONT_TABLE) ? (base.mStyle & ITALIC) != 0 : mItalic == 1;
+ return createWeightStyle(base, weight, italic);
+ }
+
+ /**
+ * Generates new Typeface from specified configuration.
+ *
+ * @return Newly created Typeface. May return null if some parameters are invalid.
+ */
+ public Typeface build() {
+ if (mFd != null) { // Builder is created with file descriptor.
+ try (FileInputStream fis = new FileInputStream(mFd)) {
+ FileChannel channel = fis.getChannel();
+ long size = channel.size();
+ ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
+
+ final FontFamily fontFamily = new FontFamily();
+ if (!fontFamily.addFontFromBuffer(buffer, mTtcIndex, mAxes, mWeight, mItalic)) {
+ fontFamily.abortCreation();
+ return resolveFallbackTypeface();
+ }
+ if (!fontFamily.freeze()) {
+ return resolveFallbackTypeface();
+ }
+ FontFamily[] families = { fontFamily };
+ return createFromFamiliesWithDefault(families, mFallbackFamilyName, mWeight,
+ mItalic);
+ } catch (IOException e) {
+ return resolveFallbackTypeface();
+ }
+ } else if (mAssetManager != null) { // Builder is created with asset manager.
+ final String key = createAssetUid(
+ mAssetManager, mPath, mTtcIndex, mAxes, mWeight, mItalic,
+ mFallbackFamilyName);
+ synchronized (sDynamicCacheLock) {
+ Typeface typeface = sDynamicTypefaceCache.get(key);
+ if (typeface != null) return typeface;
+ final FontFamily fontFamily = new FontFamily();
+ if (!fontFamily.addFontFromAssetManager(mAssetManager, mPath, mTtcIndex,
+ true /* isAsset */, mTtcIndex, mWeight, mItalic, mAxes)) {
+ fontFamily.abortCreation();
+ return resolveFallbackTypeface();
+ }
+ if (!fontFamily.freeze()) {
+ return resolveFallbackTypeface();
+ }
+ FontFamily[] families = { fontFamily };
+ typeface = createFromFamiliesWithDefault(families, mFallbackFamilyName,
+ mWeight, mItalic);
+ sDynamicTypefaceCache.put(key, typeface);
+ return typeface;
+ }
+ } else if (mPath != null) { // Builder is created with file path.
+ final FontFamily fontFamily = new FontFamily();
+ if (!fontFamily.addFont(mPath, mTtcIndex, mAxes, mWeight, mItalic)) {
+ fontFamily.abortCreation();
+ return resolveFallbackTypeface();
+ }
+ if (!fontFamily.freeze()) {
+ return resolveFallbackTypeface();
+ }
+ FontFamily[] families = { fontFamily };
+ return createFromFamiliesWithDefault(families, mFallbackFamilyName, mWeight,
+ mItalic);
+ } else if (mFonts != null) {
+ final FontFamily fontFamily = new FontFamily();
+ boolean atLeastOneFont = false;
+ for (FontsContract.FontInfo font : mFonts) {
+ final ByteBuffer fontBuffer = mFontBuffers.get(font.getUri());
+ if (fontBuffer == null) {
+ continue; // skip
+ }
+ final boolean success = fontFamily.addFontFromBuffer(fontBuffer,
+ font.getTtcIndex(), font.getAxes(), font.getWeight(),
+ font.isItalic() ? STYLE_ITALIC : STYLE_NORMAL);
+ if (!success) {
+ fontFamily.abortCreation();
+ return null;
+ }
+ atLeastOneFont = true;
+ }
+ if (!atLeastOneFont) {
+ // No fonts are avaialble. No need to create new Typeface and returns fallback
+ // Typeface instead.
+ fontFamily.abortCreation();
+ return null;
+ }
+ fontFamily.freeze();
+ FontFamily[] families = { fontFamily };
+ return createFromFamiliesWithDefault(families, mFallbackFamilyName, mWeight,
+ mItalic);
+ }
+
+ // Must not reach here.
+ throw new IllegalArgumentException("No source was set.");
+ }
+ }
+
+ /**
+ * Create a typeface object given a family name, and option style information.
+ * If null is passed for the name, then the "default" font will be chosen.
+ * The resulting typeface object can be queried (getStyle()) to discover what
+ * its "real" style characteristics are.
+ *
+ * @param familyName May be null. The name of the font family.
+ * @param style The style (normal, bold, italic) of the typeface.
+ * e.g. NORMAL, BOLD, ITALIC, BOLD_ITALIC
+ * @return The best matching typeface.
+ */
+ public static Typeface create(String familyName, int style) {
+ return create(sSystemFontMap.get(familyName), style);
+ }
+
+ /**
+ * Create a typeface object that best matches the specified existing
+ * typeface and the specified Style. Use this call if you want to pick a new
+ * style from the same family of an existing typeface object. If family is
+ * null, this selects from the default font's family.
+ *
+ * <p>
+ * This method is not thread safe on API 27 or before.
+ * This method is thread safe on API 28 or after.
+ * </p>
+ *
+ * @param family An existing {@link Typeface} object. In case of {@code null}, the default
+ * typeface is used instead.
+ * @param style The style (normal, bold, italic) of the typeface.
+ * e.g. NORMAL, BOLD, ITALIC, BOLD_ITALIC
+ * @return The best matching typeface.
+ */
+ public static Typeface create(Typeface family, int style) {
+ if ((style & ~STYLE_MASK) != 0) {
+ style = NORMAL;
+ }
+ if (family == null) {
+ family = sDefaultTypeface;
+ }
+
+ // Return early if we're asked for the same face/style
+ if (family.mStyle == style) {
+ return family;
+ }
+
+ final long ni = family.native_instance;
+
+ Typeface typeface;
+ synchronized (sStyledCacheLock) {
+ SparseArray<Typeface> styles = sStyledTypefaceCache.get(ni);
+
+ if (styles == null) {
+ styles = new SparseArray<Typeface>(4);
+ sStyledTypefaceCache.put(ni, styles);
+ } else {
+ typeface = styles.get(style);
+ if (typeface != null) {
+ return typeface;
+ }
+ }
+
+ typeface = new Typeface(nativeCreateFromTypeface(ni, style));
+ styles.put(style, typeface);
+ }
+ return typeface;
+ }
+
+ /**
+ * Creates a typeface object that best matches the specified existing typeface and the specified
+ * weight and italic style
+ *
+ * <p>
+ * This method is thread safe.
+ * </p>
+ *
+ * @param family An existing {@link Typeface} object. In case of {@code null}, the default
+ * typeface is used instead.
+ * @param weight The desired weight to be drawn.
+ * @param italic {@code true} if italic style is desired to be drawn. Otherwise, {@code false}
+ * @return A {@link Typeface} object for drawing specified weight and italic style. Never
+ * returns {@code null}
+ */
+ public static @NonNull Typeface create(@Nullable Typeface family,
+ @IntRange(from = 1, to = 1000) int weight, boolean italic) {
+ Preconditions.checkArgumentInRange(weight, 0, 1000, "weight");
+ if (family == null) {
+ family = sDefaultTypeface;
+ }
+ return createWeightStyle(family, weight, italic);
+ }
+
+ private static @NonNull Typeface createWeightStyle(@NonNull Typeface base,
+ @IntRange(from = 1, to = 1000) int weight, boolean italic) {
+ final int key = (weight << 1) | (italic ? 1 : 0);
+
+ Typeface typeface;
+ synchronized(sWeightCacheLock) {
+ SparseArray<Typeface> innerCache = sWeightTypefaceCache.get(base.native_instance);
+ if (innerCache == null) {
+ innerCache = new SparseArray<>(4);
+ sWeightTypefaceCache.put(base.native_instance, innerCache);
+ } else {
+ typeface = innerCache.get(key);
+ if (typeface != null) {
+ return typeface;
+ }
+ }
+
+ typeface = new Typeface(
+ nativeCreateFromTypefaceWithExactStyle(
+ base.native_instance, weight, italic));
+ innerCache.put(key, typeface);
+ }
+ return typeface;
+ }
+
+ /** @hide */
+ public static Typeface createFromTypefaceWithVariation(@Nullable Typeface family,
+ @NonNull List<FontVariationAxis> axes) {
+ final long ni = family == null ? 0 : family.native_instance;
+ return new Typeface(nativeCreateFromTypefaceWithVariation(ni, axes));
+ }
+
+ /**
+ * Returns one of the default typeface objects, based on the specified style
+ *
+ * @return the default typeface that corresponds to the style
+ */
+ public static Typeface defaultFromStyle(int style) {
+ return sDefaults[style];
+ }
+
+ /**
+ * Create a new typeface from the specified font data.
+ *
+ * @param mgr The application's asset manager
+ * @param path The file name of the font data in the assets directory
+ * @return The new typeface.
+ */
+ public static Typeface createFromAsset(AssetManager mgr, String path) {
+ if (path == null) {
+ throw new NullPointerException(); // for backward compatibility
+ }
+ synchronized (sDynamicCacheLock) {
+ Typeface typeface = new Builder(mgr, path).build();
+ if (typeface != null) return typeface;
+
+ final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */,
+ null /* axes */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
+ DEFAULT_FAMILY);
+ typeface = sDynamicTypefaceCache.get(key);
+ if (typeface != null) return typeface;
+
+ final FontFamily fontFamily = new FontFamily();
+ if (fontFamily.addFontFromAssetManager(mgr, path, 0, true /* isAsset */,
+ 0 /* ttc index */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
+ null /* axes */)) {
+ // Due to backward compatibility, even if the font is not supported by our font
+ // stack, we need to place the empty font at the first place. The typeface with
+ // empty font behaves different from default typeface especially in fallback
+ // font selection.
+ fontFamily.allowUnsupportedFont();
+ fontFamily.freeze();
+ final FontFamily[] families = { fontFamily };
+ typeface = createFromFamiliesWithDefault(families, DEFAULT_FAMILY,
+ RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
+ sDynamicTypefaceCache.put(key, typeface);
+ return typeface;
+ } else {
+ fontFamily.abortCreation();
+ }
+ }
+ throw new RuntimeException("Font asset not found " + path);
+ }
+
+ /**
+ * Creates a unique id for a given font provider and query.
+ */
+ private static String createProviderUid(String authority, String query) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("provider:");
+ builder.append(authority);
+ builder.append("-");
+ builder.append(query);
+ return builder.toString();
+ }
+
+ /**
+ * Create a new typeface from the specified font file.
+ *
+ * @param path The path to the font data.
+ * @return The new typeface.
+ */
+ public static Typeface createFromFile(@Nullable File path) {
+ // For the compatibility reasons, leaving possible NPE here.
+ // See android.graphics.cts.TypefaceTest#testCreateFromFileByFileReferenceNull
+ return createFromFile(path.getAbsolutePath());
+ }
+
+ /**
+ * Create a new typeface from the specified font file.
+ *
+ * @param path The full path to the font data.
+ * @return The new typeface.
+ */
+ public static Typeface createFromFile(@Nullable String path) {
+ final FontFamily fontFamily = new FontFamily();
+ if (fontFamily.addFont(path, 0 /* ttcIndex */, null /* axes */,
+ RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)) {
+ // Due to backward compatibility, even if the font is not supported by our font
+ // stack, we need to place the empty font at the first place. The typeface with
+ // empty font behaves different from default typeface especially in fallback font
+ // selection.
+ fontFamily.allowUnsupportedFont();
+ fontFamily.freeze();
+ FontFamily[] families = { fontFamily };
+ return createFromFamiliesWithDefault(families, DEFAULT_FAMILY,
+ RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
+ } else {
+ fontFamily.abortCreation();
+ }
+ throw new RuntimeException("Font not found " + path);
+ }
+
+ /**
+ * Create a new typeface from an array of font families.
+ *
+ * @param families array of font families
+ */
+ private static Typeface createFromFamilies(FontFamily[] families) {
+ long[] ptrArray = new long[families.length];
+ for (int i = 0; i < families.length; i++) {
+ ptrArray[i] = families[i].mNativePtr;
+ }
+ return new Typeface(nativeCreateFromArray(
+ ptrArray, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ }
+
+ /**
+ * Create a new typeface from an array of font families, including
+ * also the font families in the fallback list.
+ * @param fallbackName the family name. If given families don't support characters, the
+ * characters will be rendered with this family.
+ * @param weight the weight for this family. {@link RESOLVE_BY_FONT_TABLE} can be used. In that
+ * case, the table information in the first family's font is used. If the first
+ * family has multiple fonts, the closest to the regular weight and upright font
+ * is used.
+ * @param italic the italic information for this family. {@link RESOLVE_BY_FONT_TABLE} can be
+ * used. In that case, the table information in the first family's font is used.
+ * If the first family has multiple fonts, the closest to the regular weight and
+ * upright font is used.
+ * @param families array of font families
+ */
+ private static Typeface createFromFamiliesWithDefault(FontFamily[] families,
+ String fallbackName, int weight, int italic) {
+ FontFamily[] fallback = sSystemFallbackMap.get(fallbackName);
+ if (fallback == null) {
+ fallback = sSystemFallbackMap.get(DEFAULT_FAMILY);
+ }
+ long[] ptrArray = new long[families.length + fallback.length];
+ for (int i = 0; i < families.length; i++) {
+ ptrArray[i] = families[i].mNativePtr;
+ }
+ for (int i = 0; i < fallback.length; i++) {
+ ptrArray[i + families.length] = fallback[i].mNativePtr;
+ }
+ return new Typeface(nativeCreateFromArray(ptrArray, weight, italic));
+ }
+
+ // don't allow clients to call this directly
+ private Typeface(long ni) {
+ if (ni == 0) {
+ throw new RuntimeException("native typeface cannot be made");
+ }
+
+ native_instance = ni;
+ mStyle = nativeGetStyle(ni);
+ mWeight = nativeGetWeight(ni);
+ }
+
+ private static @Nullable ByteBuffer mmap(String fullPath) {
+ try (FileInputStream file = new FileInputStream(fullPath)) {
+ final FileChannel fileChannel = file.getChannel();
+ final long fontSize = fileChannel.size();
+ return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
+ } catch (IOException e) {
+ Log.e(TAG, "Error mapping font file " + fullPath);
+ return null;
+ }
+ }
+
+ private static @Nullable FontFamily createFontFamily(
+ String familyName, List<FontConfig.Font> fonts, String[] languageTags, int variant,
+ Map<String, ByteBuffer> cache, String fontDir) {
+ final FontFamily family = new FontFamily(languageTags, variant);
+ for (int i = 0; i < fonts.size(); i++) {
+ final FontConfig.Font font = fonts.get(i);
+ final String fullPath = fontDir + font.getFontName();
+ ByteBuffer buffer = cache.get(fullPath);
+ if (buffer == null) {
+ if (cache.containsKey(fullPath)) {
+ continue; // Already failed to mmap. Skip it.
+ }
+ buffer = mmap(fullPath);
+ cache.put(fullPath, buffer);
+ if (buffer == null) {
+ continue;
+ }
+ }
+ if (!family.addFontFromBuffer(buffer, font.getTtcIndex(), font.getAxes(),
+ font.getWeight(), font.isItalic() ? STYLE_ITALIC : STYLE_NORMAL)) {
+ Log.e(TAG, "Error creating font " + fullPath + "#" + font.getTtcIndex());
+ }
+ }
+ if (!family.freeze()) {
+ Log.e(TAG, "Unable to load Family: " + familyName + " : "
+ + Arrays.toString(languageTags));
+ return null;
+ }
+ return family;
+ }
+
+ private static void pushFamilyToFallback(FontConfig.Family xmlFamily,
+ ArrayMap<String, ArrayList<FontFamily>> fallbackMap,
+ Map<String, ByteBuffer> cache,
+ String fontDir) {
+
+ final String[] languageTags = xmlFamily.getLanguages();
+ final int variant = xmlFamily.getVariant();
+
+ final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>();
+ final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts = new ArrayMap<>();
+
+ // Collect default fallback and specific fallback fonts.
+ for (final FontConfig.Font font : xmlFamily.getFonts()) {
+ final String fallbackName = font.getFallbackFor();
+ if (fallbackName == null) {
+ defaultFonts.add(font);
+ } else {
+ ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName);
+ if (fallback == null) {
+ fallback = new ArrayList<>();
+ specificFallbackFonts.put(fallbackName, fallback);
+ }
+ fallback.add(font);
+ }
+ }
+
+ final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily(
+ xmlFamily.getName(), defaultFonts, languageTags, variant, cache, fontDir);
+
+ // Insert family into fallback map.
+ for (int i = 0; i < fallbackMap.size(); i++) {
+ final ArrayList<FontConfig.Font> fallback =
+ specificFallbackFonts.get(fallbackMap.keyAt(i));
+ if (fallback == null) {
+ if (defaultFamily != null) {
+ fallbackMap.valueAt(i).add(defaultFamily);
+ }
+ } else {
+ final FontFamily family = createFontFamily(
+ xmlFamily.getName(), fallback, languageTags, variant, cache, fontDir);
+ if (family != null) {
+ fallbackMap.valueAt(i).add(family);
+ }
+ }
+ }
+ }
+
+ /**
+ * Build the system fallback from xml file.
+ *
+ * @param xmlPath A full path string to the fonts.xml file.
+ * @param fontDir A full path string to the system font directory. This must end with
+ * slash('/').
+ * @param fontMap An output system font map. Caller must pass empty map.
+ * @param fallbackMap An output system fallback map. Caller must pass empty map.
+ * @hide
+ */
+ @VisibleForTesting
+ public static void buildSystemFallback(String xmlPath, String fontDir,
+ ArrayMap<String, Typeface> fontMap, ArrayMap<String, FontFamily[]> fallbackMap) {
+ try {
+ final FileInputStream fontsIn = new FileInputStream(xmlPath);
+ final FontConfig fontConfig = FontListParser.parse(fontsIn);
+
+ final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>();
+ final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies();
+
+ final ArrayMap<String, ArrayList<FontFamily>> fallbackListMap = new ArrayMap<>();
+ // First traverse families which have a 'name' attribute to create fallback map.
+ for (final FontConfig.Family xmlFamily : xmlFamilies) {
+ final String familyName = xmlFamily.getName();
+ if (familyName == null) {
+ continue;
+ }
+ final FontFamily family = createFontFamily(
+ xmlFamily.getName(), Arrays.asList(xmlFamily.getFonts()),
+ xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, fontDir);
+ if (family == null) {
+ continue;
+ }
+ final ArrayList<FontFamily> fallback = new ArrayList<>();
+ fallback.add(family);
+ fallbackListMap.put(familyName, fallback);
+ }
+
+ // Then, add fallback fonts to the each fallback map.
+ for (int i = 0; i < xmlFamilies.length; i++) {
+ final FontConfig.Family xmlFamily = xmlFamilies[i];
+ // The first family (usually the sans-serif family) is always placed immediately
+ // after the primary family in the fallback.
+ if (i == 0 || xmlFamily.getName() == null) {
+ pushFamilyToFallback(xmlFamily, fallbackListMap, bufferCache, fontDir);
+ }
+ }
+
+ // Build the font map and fallback map.
+ for (int i = 0; i < fallbackListMap.size(); i++) {
+ final String fallbackName = fallbackListMap.keyAt(i);
+ final List<FontFamily> familyList = fallbackListMap.valueAt(i);
+ final FontFamily[] families = familyList.toArray(new FontFamily[familyList.size()]);
+
+ fallbackMap.put(fallbackName, families);
+ final long[] ptrArray = new long[families.length];
+ for (int j = 0; j < families.length; j++) {
+ ptrArray[j] = families[j].mNativePtr;
+ }
+ fontMap.put(fallbackName, new Typeface(nativeCreateFromArray(
+ ptrArray, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)));
+ }
+
+ // Insert alias to font maps.
+ for (final FontConfig.Alias alias : fontConfig.getAliases()) {
+ Typeface base = fontMap.get(alias.getToName());
+ Typeface newFace = base;
+ int weight = alias.getWeight();
+ if (weight != 400) {
+ newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight));
+ }
+ fontMap.put(alias.getName(), newFace);
+ }
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e);
+ // TODO: normal in non-Minikin case, remove or make error when Minikin-only
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Error opening " + xmlPath, e);
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading " + xmlPath, e);
+ } catch (XmlPullParserException e) {
+ Log.e(TAG, "XML parse exception for " + xmlPath, e);
+ }
+ }
+
+ static {
+ final ArrayMap<String, Typeface> systemFontMap = new ArrayMap<>();
+ final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>();
+ buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", systemFontMap,
+ systemFallbackMap);
+ sSystemFontMap = Collections.unmodifiableMap(systemFontMap);
+ sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap);
+
+ setDefault(sSystemFontMap.get(DEFAULT_FAMILY));
+
+ // Set up defaults and typefaces exposed in public API
+ DEFAULT = create((String) null, 0);
+ DEFAULT_BOLD = create((String) null, Typeface.BOLD);
+ SANS_SERIF = create("sans-serif", 0);
+ SERIF = create("serif", 0);
+ MONOSPACE = create("monospace", 0);
+
+ sDefaults = new Typeface[] {
+ DEFAULT,
+ DEFAULT_BOLD,
+ create((String) null, Typeface.ITALIC),
+ create((String) null, Typeface.BOLD_ITALIC),
+ };
+
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ nativeUnref(native_instance);
+ native_instance = 0; // Other finalizers can still call us.
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Typeface typeface = (Typeface) o;
+
+ return mStyle == typeface.mStyle && native_instance == typeface.native_instance;
+ }
+
+ @Override
+ public int hashCode() {
+ /*
+ * Modified method for hashCode with long native_instance derived from
+ * http://developer.android.com/reference/java/lang/Object.html
+ */
+ int result = 17;
+ result = 31 * result + (int) (native_instance ^ (native_instance >>> 32));
+ result = 31 * result + mStyle;
+ return result;
+ }
+
+ /** @hide */
+ public boolean isSupportedAxes(int axis) {
+ if (mSupportedAxes == null) {
+ synchronized (this) {
+ if (mSupportedAxes == null) {
+ mSupportedAxes = nativeGetSupportedAxes(native_instance);
+ if (mSupportedAxes == null) {
+ mSupportedAxes = EMPTY_AXES;
+ }
+ }
+ }
+ }
+ return Arrays.binarySearch(mSupportedAxes, axis) >= 0;
+ }
+
+ private static native long nativeCreateFromTypeface(long native_instance, int style);
+ private static native long nativeCreateFromTypefaceWithExactStyle(
+ long native_instance, int weight, boolean italic);
+ // TODO: clean up: change List<FontVariationAxis> to FontVariationAxis[]
+ private static native long nativeCreateFromTypefaceWithVariation(
+ long native_instance, List<FontVariationAxis> axes);
+ private static native long nativeCreateWeightAlias(long native_instance, int weight);
+ private static native void nativeUnref(long native_instance);
+ private static native int nativeGetStyle(long native_instance);
+ private static native int nativeGetWeight(long native_instance);
+ private static native long nativeCreateFromArray(long[] familyArray, int weight, int italic);
+ private static native void nativeSetDefault(long native_instance);
+ private static native int[] nativeGetSupportedAxes(long native_instance);
+}
diff --git a/android/graphics/Typeface_Accessor.java b/android/graphics/Typeface_Accessor.java
new file mode 100644
index 0000000..ce669cb
--- /dev/null
+++ b/android/graphics/Typeface_Accessor.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 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 android.graphics;
+
+import android.annotation.NonNull;
+
+/**
+ * Class allowing access to package-protected methods/fields.
+ */
+public class Typeface_Accessor {
+ public static boolean isSystemFont(@NonNull String fontName) {
+ return Typeface.sSystemFontMap.containsKey(fontName);
+ }
+}
diff --git a/android/graphics/Typeface_Delegate.java b/android/graphics/Typeface_Delegate.java
new file mode 100644
index 0000000..b9c0353
--- /dev/null
+++ b/android/graphics/Typeface_Delegate.java
@@ -0,0 +1,401 @@
+/*
+ * 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 android.graphics;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.FontFamily_Delegate.FontVariant;
+import android.graphics.fonts.FontVariationAxis;
+import android.text.FontConfig;
+import android.util.ArrayMap;
+
+import java.awt.Font;
+import java.lang.ref.SoftReference;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Spliterator;
+import java.util.Spliterators;
+
+import static android.graphics.FontFamily_Delegate.getFontLocation;
+
+/**
+ * Delegate implementing the native methods of android.graphics.Typeface
+ * <p>
+ * Through the layoutlib_create tool, the original native methods of Typeface have been replaced by
+ * calls to methods of the same name in this delegate class.
+ * <p>
+ * This class behaves like the original native implementation, but in Java, keeping previously
+ * native data into its own objects and mapping them to int that are sent back and forth between it
+ * and the original Typeface class.
+ *
+ * @see DelegateManager
+ */
+public final class Typeface_Delegate {
+
+ public static final String SYSTEM_FONTS = "/system/fonts/";
+
+ // ---- delegate manager ----
+ private static final DelegateManager<Typeface_Delegate> sManager =
+ new DelegateManager<>(Typeface_Delegate.class);
+
+
+ // ---- delegate data ----
+ private static long sDefaultTypeface;
+ @NonNull
+ private final FontFamily_Delegate[] mFontFamilies; // the reference to FontFamily_Delegate.
+ /** @see Font#getStyle() */
+ private final int mStyle;
+ private final int mWeight;
+ private SoftReference<EnumMap<FontVariant, List<Font>>> mFontsCache = new SoftReference<>(null);
+
+
+ // ---- Public Helper methods ----
+
+ public Typeface_Delegate(@NonNull FontFamily_Delegate[] fontFamilies, int style, int weight) {
+ mFontFamilies = fontFamilies;
+ mStyle = style;
+ mWeight = weight;
+ }
+
+ public static Typeface_Delegate getDelegate(long nativeTypeface) {
+ return sManager.getDelegate(nativeTypeface);
+ }
+
+ /**
+ * Clear the default typefaces when disposing bridge.
+ */
+ public static void resetDefaults() {
+ // Sometimes this is called before the Bridge is initialized. In that case, we don't want to
+ // initialize Typeface because the SDK fonts location hasn't been set.
+ if (FontFamily_Delegate.getFontLocation() != null) {
+ Typeface.sDefaults = null;
+ }
+ }
+
+
+ // ---- native methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static synchronized long nativeCreateFromTypeface(long native_instance, int style) {
+ Typeface_Delegate delegate = sManager.getDelegate(native_instance);
+ if (delegate == null) {
+ delegate = sManager.getDelegate(sDefaultTypeface);
+ }
+ if (delegate == null) {
+ return 0;
+ }
+
+ return sManager.addNewDelegate(
+ new Typeface_Delegate(delegate.mFontFamilies, style, delegate.mWeight));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreateFromTypefaceWithExactStyle(long native_instance, int weight,
+ boolean italic) {
+ Typeface_Delegate delegate = sManager.getDelegate(native_instance);
+ if (delegate == null) {
+ delegate = sManager.getDelegate(sDefaultTypeface);
+ }
+ if (delegate == null) {
+ return 0;
+ }
+
+ int style = weight >= 600 ? (italic ? Typeface.BOLD_ITALIC : Typeface.BOLD) :
+ (italic ? Typeface.ITALIC : Typeface.NORMAL);
+ return sManager.addNewDelegate(
+ new Typeface_Delegate(delegate.mFontFamilies, style, weight));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static synchronized long nativeCreateFromTypefaceWithVariation(long native_instance,
+ List<FontVariationAxis> axes) {
+ long newInstance = nativeCreateFromTypeface(native_instance, 0);
+
+ if (newInstance != 0) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
+ "nativeCreateFromTypefaceWithVariation is not supported", null, null);
+ }
+ return newInstance;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static synchronized int[] nativeGetSupportedAxes(long native_instance) {
+ // nativeCreateFromTypefaceWithVariation is not supported so we do not keep the axes
+ return null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreateWeightAlias(long native_instance, int weight) {
+ Typeface_Delegate delegate = sManager.getDelegate(native_instance);
+ if (delegate == null) {
+ delegate = sManager.getDelegate(sDefaultTypeface);
+ }
+ if (delegate == null) {
+ return 0;
+ }
+ Typeface_Delegate weightAlias =
+ new Typeface_Delegate(delegate.mFontFamilies, delegate.mStyle, weight);
+ return sManager.addNewDelegate(weightAlias);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static synchronized long nativeCreateFromArray(long[] familyArray, int weight,
+ int italic) {
+ FontFamily_Delegate[] fontFamilies = new FontFamily_Delegate[familyArray.length];
+ for (int i = 0; i < familyArray.length; i++) {
+ fontFamilies[i] = FontFamily_Delegate.getDelegate(familyArray[i]);
+ }
+ if (weight == Typeface.RESOLVE_BY_FONT_TABLE) {
+ weight = 400;
+ }
+ if (italic == Typeface.RESOLVE_BY_FONT_TABLE) {
+ italic = 0;
+ }
+ int style = weight >= 600 ? (italic == 1 ? Typeface.BOLD_ITALIC : Typeface.BOLD) :
+ (italic == 1 ? Typeface.ITALIC : Typeface.NORMAL);
+ Typeface_Delegate delegate = new Typeface_Delegate(fontFamilies, style, weight);
+ return sManager.addNewDelegate(delegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeUnref(long native_instance) {
+ sManager.removeJavaReferenceFor(native_instance);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nativeGetStyle(long native_instance) {
+ Typeface_Delegate delegate = sManager.getDelegate(native_instance);
+ if (delegate == null) {
+ return 0;
+ }
+
+ return delegate.mStyle;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nativeSetDefault(long native_instance) {
+ sDefaultTypeface = native_instance;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static int nativeGetWeight(long native_instance) {
+ Typeface_Delegate delegate = sManager.getDelegate(native_instance);
+ if (delegate == null) {
+ return 0;
+ }
+ return delegate.mWeight;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void buildSystemFallback(String xmlPath, String fontDir,
+ ArrayMap<String, Typeface> fontMap, ArrayMap<String, FontFamily[]> fallbackMap) {
+ Typeface.buildSystemFallback_Original(getFontLocation() + "/fonts.xml", fontDir, fontMap,
+ fallbackMap);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static FontFamily createFontFamily(String familyName, List<FontConfig.Font> fonts,
+ String[] languageTags, int variant, Map<String, ByteBuffer> cache, String fontDir) {
+ FontFamily fontFamily = new FontFamily(languageTags, variant);
+ for (FontConfig.Font font : fonts) {
+ String fullPathName = fontDir + font.getFontName();
+ FontFamily_Delegate.addFont(fontFamily.mBuilderPtr, fullPathName, font.getWeight(),
+ font.isItalic());
+ }
+ fontFamily.freeze();
+ return fontFamily;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Typeface create(String familyName, int style) {
+ if (familyName != null && Files.exists(Paths.get(familyName))) {
+ // Workaround for b/64137851
+ // Support lib will call this method after failing to create the TypefaceCompat.
+ return Typeface.createFromFile(familyName);
+ }
+ return Typeface.create_Original(familyName, style);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Typeface create(Typeface family, int style) {
+ return Typeface.create_Original(family, style);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static Typeface create(Typeface family, int style, boolean isItalic) {
+ return Typeface.create_Original(family, style, isItalic);
+ }
+
+ // ---- Private delegate/helper methods ----
+
+ private static List<Font> computeFonts(FontVariant variant, FontFamily_Delegate[] fontFamilies,
+ int inputWeight, int inputStyle) {
+ // Calculate the required weight based on style and weight of this typeface.
+ int weight = inputWeight + 50 +
+ ((inputStyle & Font.BOLD) == 0 ? 0 : FontFamily_Delegate.BOLD_FONT_WEIGHT_DELTA);
+ if (weight > 1000) {
+ weight = 1000;
+ } else if (weight < 100) {
+ weight = 100;
+ }
+ final boolean isItalic = (inputStyle & Font.ITALIC) != 0;
+ List<Font> fonts = new ArrayList<Font>(fontFamilies.length);
+ for (int i = 0; i < fontFamilies.length; i++) {
+ FontFamily_Delegate ffd = fontFamilies[i];
+ if (ffd != null && ffd.isValid()) {
+ Font font = ffd.getFont(weight, isItalic);
+ if (font != null) {
+ FontVariant ffdVariant = ffd.getVariant();
+ if (ffdVariant == FontVariant.NONE) {
+ fonts.add(font);
+ continue;
+ }
+ // We cannot open each font and get locales supported, etc to match the fonts.
+ // As a workaround, we hardcode certain assumptions like Elegant and Compact
+ // always appear in pairs.
+ assert i < fontFamilies.length - 1;
+ FontFamily_Delegate ffd2 = fontFamilies[++i];
+ assert ffd2 != null;
+ FontVariant ffd2Variant = ffd2.getVariant();
+ Font font2 = ffd2.getFont(weight, isItalic);
+ assert ffd2Variant != FontVariant.NONE && ffd2Variant != ffdVariant &&
+ font2 != null;
+ // Add the font with the matching variant to the list.
+ if (variant == ffd.getVariant()) {
+ fonts.add(font);
+ } else {
+ fonts.add(font2);
+ }
+ } else {
+ // The FontFamily is valid but doesn't contain any matching font. This means
+ // that the font failed to load. We add null to the list of fonts. Don't throw
+ // the warning just yet. If this is a non-english font, we don't want to warn
+ // users who are trying to render only english text.
+ fonts.add(null);
+ }
+ }
+ }
+
+ return fonts;
+ }
+
+ /**
+ * Return an Iterable of fonts that match the style and variant. The list is ordered
+ * according to preference of fonts.
+ * <p>
+ * The Iterator may contain null when the font failed to load. If null is reached when trying to
+ * render with this list of fonts, then a warning should be logged letting the user know that
+ * some font failed to load.
+ *
+ * @param variant The variant preferred. Can only be {@link FontVariant#COMPACT} or {@link
+ * FontVariant#ELEGANT}
+ */
+ @NonNull
+ public Iterable<Font> getFonts(final FontVariant variant) {
+ assert variant != FontVariant.NONE;
+
+ return new FontsIterator(mFontFamilies, variant, mWeight, mStyle);
+ }
+
+ private static class FontsIterator implements Iterator<Font>, Iterable<Font> {
+ private final FontFamily_Delegate[] fontFamilies;
+ private final int weight;
+ private final boolean isItalic;
+ private final FontVariant variant;
+
+ private int index = 0;
+
+ private FontsIterator(@NonNull FontFamily_Delegate[] fontFamilies,
+ @NonNull FontVariant variant, int weight, int style) {
+ // Calculate the required weight based on style and weight of this typeface.
+ int boldExtraWeight =
+ ((style & Font.BOLD) == 0 ? 0 : FontFamily_Delegate.BOLD_FONT_WEIGHT_DELTA);
+ this.weight = Math.min(Math.max(100, weight + 50 + boldExtraWeight), 1000);
+ this.isItalic = (style & Font.ITALIC) != 0;
+ this.fontFamilies = fontFamilies;
+ this.variant = variant;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return index < fontFamilies.length;
+ }
+
+ @Override
+ @Nullable
+ public Font next() {
+ FontFamily_Delegate ffd = fontFamilies[index++];
+ if (ffd == null || !ffd.isValid()) {
+ return null;
+ }
+
+ Font font = ffd.getFont(weight, isItalic);
+ if (font == null) {
+ // The FontFamily is valid but doesn't contain any matching font. This means
+ // that the font failed to load. We add null to the list of fonts. Don't throw
+ // the warning just yet. If this is a non-english font, we don't want to warn
+ // users who are trying to render only english text.
+ return null;
+ }
+
+ FontVariant ffdVariant = ffd.getVariant();
+ if (ffdVariant == FontVariant.NONE) {
+ return font;
+ }
+
+ // We cannot open each font and get locales supported, etc to match the fonts.
+ // As a workaround, we hardcode certain assumptions like Elegant and Compact
+ // always appear in pairs.
+ assert index < fontFamilies.length - 1;
+ FontFamily_Delegate ffd2 = fontFamilies[index++];
+ assert ffd2 != null;
+
+ if (ffdVariant == variant) {
+ return font;
+ }
+
+ FontVariant ffd2Variant = ffd2.getVariant();
+ Font font2 = ffd2.getFont(weight, isItalic);
+ assert ffd2Variant != FontVariant.NONE && ffd2Variant != ffdVariant && font2 != null;
+ // Add the font with the matching variant to the list.
+ return variant == ffd.getVariant() ? font : font2;
+ }
+
+ @NonNull
+ @Override
+ public Iterator<Font> iterator() {
+ return this;
+ }
+
+ @Override
+ public Spliterator<Font> spliterator() {
+ return Spliterators.spliterator(iterator(), fontFamilies.length,
+ Spliterator.IMMUTABLE | Spliterator.SIZED);
+ }
+ }
+}
diff --git a/android/graphics/Xfermode.java b/android/graphics/Xfermode.java
new file mode 100644
index 0000000..a5da5d0
--- /dev/null
+++ b/android/graphics/Xfermode.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2006 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.
+ */
+
+// This file was generated from the C++ include file: SkXfermode.h
+// Any changes made to this file will be discarded by the build.
+// To change this file, either edit the include, or device/tools/gluemaker/main.cpp,
+// or one of the auxilary file specifications in device/tools/gluemaker.
+
+package android.graphics;
+
+/**
+ * Xfermode is the base class for objects that are called to implement custom
+ * "transfer-modes" in the drawing pipeline. The static function Create(Modes)
+ * can be called to return an instance of any of the predefined subclasses as
+ * specified in the Modes enum. When an Xfermode is assigned to an Paint, then
+ * objects drawn with that paint have the xfermode applied.
+ */
+public class Xfermode {
+ static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt;
+ int porterDuffMode = DEFAULT;
+}
diff --git a/android/graphics/YuvImage.java b/android/graphics/YuvImage.java
new file mode 100644
index 0000000..af3f276
--- /dev/null
+++ b/android/graphics/YuvImage.java
@@ -0,0 +1,240 @@
+/*
+ * 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 android.graphics;
+
+import java.io.OutputStream;
+
+/**
+ * YuvImage contains YUV data and provides a method that compresses a region of
+ * the YUV data to a Jpeg. The YUV data should be provided as a single byte
+ * array irrespective of the number of image planes in it.
+ * Currently only ImageFormat.NV21 and ImageFormat.YUY2 are supported.
+ *
+ * To compress a rectangle region in the YUV data, users have to specify the
+ * region by left, top, width and height.
+ */
+public class YuvImage {
+
+ /**
+ * Number of bytes of temp storage we use for communicating between the
+ * native compressor and the java OutputStream.
+ */
+ private final static int WORKING_COMPRESS_STORAGE = 4096;
+
+ /**
+ * The YUV format as defined in {@link ImageFormat}.
+ */
+ private int mFormat;
+
+ /**
+ * The raw YUV data.
+ * In the case of more than one image plane, the image planes must be
+ * concatenated into a single byte array.
+ */
+ private byte[] mData;
+
+ /**
+ * The number of row bytes in each image plane.
+ */
+ private int[] mStrides;
+
+ /**
+ * The width of the image.
+ */
+ private int mWidth;
+
+ /**
+ * The height of the the image.
+ */
+ private int mHeight;
+
+ /**
+ * Construct an YuvImage.
+ *
+ * @param yuv The YUV data. In the case of more than one image plane, all the planes must be
+ * concatenated into a single byte array.
+ * @param format The YUV data format as defined in {@link ImageFormat}.
+ * @param width The width of the YuvImage.
+ * @param height The height of the YuvImage.
+ * @param strides (Optional) Row bytes of each image plane. If yuv contains padding, the stride
+ * of each image must be provided. If strides is null, the method assumes no
+ * padding and derives the row bytes by format and width itself.
+ * @throws IllegalArgumentException if format is not support; width or height <= 0; or yuv is
+ * null.
+ */
+ public YuvImage(byte[] yuv, int format, int width, int height, int[] strides) {
+ if (format != ImageFormat.NV21 &&
+ format != ImageFormat.YUY2) {
+ throw new IllegalArgumentException(
+ "only support ImageFormat.NV21 " +
+ "and ImageFormat.YUY2 for now");
+ }
+
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException(
+ "width and height must large than 0");
+ }
+
+ if (yuv == null) {
+ throw new IllegalArgumentException("yuv cannot be null");
+ }
+
+ if (strides == null) {
+ mStrides = calculateStrides(width, format);
+ } else {
+ mStrides = strides;
+ }
+
+ mData = yuv;
+ mFormat = format;
+ mWidth = width;
+ mHeight = height;
+ }
+
+ /**
+ * Compress a rectangle region in the YuvImage to a jpeg.
+ * Only ImageFormat.NV21 and ImageFormat.YUY2
+ * are supported for now.
+ *
+ * @param rectangle The rectangle region to be compressed. The medthod checks if rectangle is
+ * inside the image. Also, the method modifies rectangle if the chroma pixels
+ * in it are not matched with the luma pixels in it.
+ * @param quality Hint to the compressor, 0-100. 0 meaning compress for
+ * small size, 100 meaning compress for max quality.
+ * @param stream OutputStream to write the compressed data.
+ * @return True if the compression is successful.
+ * @throws IllegalArgumentException if rectangle is invalid; quality is not within [0,
+ * 100]; or stream is null.
+ */
+ public boolean compressToJpeg(Rect rectangle, int quality, OutputStream stream) {
+ Rect wholeImage = new Rect(0, 0, mWidth, mHeight);
+ if (!wholeImage.contains(rectangle)) {
+ throw new IllegalArgumentException(
+ "rectangle is not inside the image");
+ }
+
+ if (quality < 0 || quality > 100) {
+ throw new IllegalArgumentException("quality must be 0..100");
+ }
+
+ if (stream == null) {
+ throw new IllegalArgumentException("stream cannot be null");
+ }
+
+ adjustRectangle(rectangle);
+ int[] offsets = calculateOffsets(rectangle.left, rectangle.top);
+
+ return nativeCompressToJpeg(mData, mFormat, rectangle.width(),
+ rectangle.height(), offsets, mStrides, quality, stream,
+ new byte[WORKING_COMPRESS_STORAGE]);
+ }
+
+
+ /**
+ * @return the YUV data.
+ */
+ public byte[] getYuvData() {
+ return mData;
+ }
+
+ /**
+ * @return the YUV format as defined in {@link ImageFormat}.
+ */
+ public int getYuvFormat() {
+ return mFormat;
+ }
+
+ /**
+ * @return the number of row bytes in each image plane.
+ */
+ public int[] getStrides() {
+ return mStrides;
+ }
+
+ /**
+ * @return the width of the image.
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * @return the height of the image.
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ int[] calculateOffsets(int left, int top) {
+ int[] offsets = null;
+ if (mFormat == ImageFormat.NV21) {
+ offsets = new int[] {top * mStrides[0] + left,
+ mHeight * mStrides[0] + top / 2 * mStrides[1]
+ + left / 2 * 2 };
+ return offsets;
+ }
+
+ if (mFormat == ImageFormat.YUY2) {
+ offsets = new int[] {top * mStrides[0] + left / 2 * 4};
+ return offsets;
+ }
+
+ return offsets;
+ }
+
+ private int[] calculateStrides(int width, int format) {
+ int[] strides = null;
+ if (format == ImageFormat.NV21) {
+ strides = new int[] {width, width};
+ return strides;
+ }
+
+ if (format == ImageFormat.YUY2) {
+ strides = new int[] {width * 2};
+ return strides;
+ }
+
+ return strides;
+ }
+
+ private void adjustRectangle(Rect rect) {
+ int width = rect.width();
+ int height = rect.height();
+ if (mFormat == ImageFormat.NV21) {
+ // Make sure left, top, width and height are all even.
+ width &= ~1;
+ height &= ~1;
+ rect.left &= ~1;
+ rect.top &= ~1;
+ rect.right = rect.left + width;
+ rect.bottom = rect.top + height;
+ }
+
+ if (mFormat == ImageFormat.YUY2) {
+ // Make sure left and width are both even.
+ width &= ~1;
+ rect.left &= ~1;
+ rect.right = rect.left + width;
+ }
+ }
+
+ //////////// native methods
+
+ private static native boolean nativeCompressToJpeg(byte[] oriYuv,
+ int format, int width, int height, int[] offsets, int[] strides,
+ int quality, OutputStream stream, byte[] tempStorage);
+}
diff --git a/android/graphics/drawable/AdaptiveIconDrawable.java b/android/graphics/drawable/AdaptiveIconDrawable.java
new file mode 100644
index 0000000..1d0cfa5
--- /dev/null
+++ b/android/graphics/drawable/AdaptiveIconDrawable.java
@@ -0,0 +1,1115 @@
+/*
+ * Copyright (C) 2017 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 android.graphics.drawable;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.Shader;
+import android.graphics.Shader.TileMode;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.PathParser;
+
+import com.android.internal.R;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * <p>This class can also be created via XML inflation using <code><adaptive-icon></code> tag
+ * in addition to dynamic creation.
+ *
+ * <p>This drawable supports two drawable layers: foreground and background. The layers are clipped
+ * when rendering using the mask defined in the device configuration.
+ *
+ * <ul>
+ * <li>Both foreground and background layers should be sized at 108 x 108 dp.</li>
+ * <li>The inner 72 x 72 dp of the icon appears within the masked viewport.</li>
+ * <li>The outer 18 dp on each of the 4 sides of the layers is reserved for use by the system UI
+ * surfaces to create interesting visual effects, such as parallax or pulsing.</li>
+ * </ul>
+ *
+ * Such motion effect is achieved by internally setting the bounds of the foreground and
+ * background layer as following:
+ * <pre>
+ * Rect(getBounds().left - getBounds().getWidth() * #getExtraInsetFraction(),
+ * getBounds().top - getBounds().getHeight() * #getExtraInsetFraction(),
+ * getBounds().right + getBounds().getWidth() * #getExtraInsetFraction(),
+ * getBounds().bottom + getBounds().getHeight() * #getExtraInsetFraction())
+ * </pre>
+ */
+public class AdaptiveIconDrawable extends Drawable implements Drawable.Callback {
+
+ /**
+ * Mask path is defined inside device configuration in following dimension: [100 x 100]
+ * @hide
+ */
+ @TestApi
+ public static final float MASK_SIZE = 100f;
+
+ /**
+ * Launcher icons design guideline
+ */
+ private static final float SAFEZONE_SCALE = 66f/72f;
+
+ /**
+ * All four sides of the layers are padded with extra inset so as to provide
+ * extra content to reveal within the clip path when performing affine transformations on the
+ * layers.
+ *
+ * Each layers will reserve 25% of it's width and height.
+ *
+ * As a result, the view port of the layers is smaller than their intrinsic width and height.
+ */
+ private static final float EXTRA_INSET_PERCENTAGE = 1 / 4f;
+ private static final float DEFAULT_VIEW_PORT_SCALE = 1f / (1 + 2 * EXTRA_INSET_PERCENTAGE);
+
+ /**
+ * Clip path defined in R.string.config_icon_mask.
+ */
+ private static Path sMask;
+
+ /**
+ * Scaled mask based on the view bounds.
+ */
+ private final Path mMask;
+ private final Matrix mMaskMatrix;
+ private final Region mTransparentRegion;
+
+ private Bitmap mMaskBitmap;
+
+ /**
+ * Indices used to access {@link #mLayerState.mChildDrawable} array for foreground and
+ * background layer.
+ */
+ private static final int BACKGROUND_ID = 0;
+ private static final int FOREGROUND_ID = 1;
+
+ /**
+ * State variable that maintains the {@link ChildDrawable} array.
+ */
+ LayerState mLayerState;
+
+ private Shader mLayersShader;
+ private Bitmap mLayersBitmap;
+
+ private final Rect mTmpOutRect = new Rect();
+ private Rect mHotspotBounds;
+ private boolean mMutated;
+
+ private boolean mSuspendChildInvalidation;
+ private boolean mChildRequestedInvalidation;
+ private final Canvas mCanvas;
+ private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG |
+ Paint.FILTER_BITMAP_FLAG);
+
+ /**
+ * Constructor used for xml inflation.
+ */
+ AdaptiveIconDrawable() {
+ this((LayerState) null, null);
+ }
+
+ /**
+ * The one constructor to rule them all. This is called by all public
+ * constructors to set the state and initialize local properties.
+ */
+ AdaptiveIconDrawable(@Nullable LayerState state, @Nullable Resources res) {
+ mLayerState = createConstantState(state, res);
+
+ if (sMask == null) {
+ sMask = PathParser.createPathFromPathData(
+ Resources.getSystem().getString(R.string.config_icon_mask));
+ }
+ mMask = PathParser.createPathFromPathData(
+ Resources.getSystem().getString(R.string.config_icon_mask));
+ mMaskMatrix = new Matrix();
+ mCanvas = new Canvas();
+ mTransparentRegion = new Region();
+ }
+
+ private ChildDrawable createChildDrawable(Drawable drawable) {
+ final ChildDrawable layer = new ChildDrawable(mLayerState.mDensity);
+ layer.mDrawable = drawable;
+ layer.mDrawable.setCallback(this);
+ mLayerState.mChildrenChangingConfigurations |=
+ layer.mDrawable.getChangingConfigurations();
+ return layer;
+ }
+
+ LayerState createConstantState(@Nullable LayerState state, @Nullable Resources res) {
+ return new LayerState(state, this, res);
+ }
+
+ /**
+ * Constructor used to dynamically create this drawable.
+ *
+ * @param backgroundDrawable drawable that should be rendered in the background
+ * @param foregroundDrawable drawable that should be rendered in the foreground
+ */
+ public AdaptiveIconDrawable(Drawable backgroundDrawable,
+ Drawable foregroundDrawable) {
+ this((LayerState)null, null);
+ if (backgroundDrawable != null) {
+ addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable));
+ }
+ if (foregroundDrawable != null) {
+ addLayer(FOREGROUND_ID, createChildDrawable(foregroundDrawable));
+ }
+ }
+
+ /**
+ * Sets the layer to the {@param index} and invalidates cache.
+ *
+ * @param index The index of the layer.
+ * @param layer The layer to add.
+ */
+ private void addLayer(int index, @NonNull ChildDrawable layer) {
+ mLayerState.mChildren[index] = layer;
+ mLayerState.invalidateCache();
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ super.inflate(r, parser, attrs, theme);
+
+ final LayerState state = mLayerState;
+ if (state == null) {
+ return;
+ }
+
+ // The density may have changed since the last update. This will
+ // apply scaling to any existing constant state properties.
+ final int deviceDensity = Drawable.resolveDensity(r, 0);
+ state.setDensity(deviceDensity);
+ state.mSrcDensityOverride = mSrcDensityOverride;
+
+ final ChildDrawable[] array = state.mChildren;
+ for (int i = 0; i < state.mChildren.length; i++) {
+ final ChildDrawable layer = array[i];
+ layer.setDensity(deviceDensity);
+ }
+
+ inflateLayers(r, parser, attrs, theme);
+ }
+
+ /**
+ * All four sides of the layers are padded with extra inset so as to provide
+ * extra content to reveal within the clip path when performing affine transformations on the
+ * layers.
+ *
+ * @see #getForeground() and #getBackground() for more info on how this value is used
+ */
+ public static float getExtraInsetFraction() {
+ return EXTRA_INSET_PERCENTAGE;
+ }
+
+ /**
+ * @hide
+ */
+ public static float getExtraInsetPercentage() {
+ return EXTRA_INSET_PERCENTAGE;
+ }
+
+ /**
+ * When called before the bound is set, the returned path is identical to
+ * R.string.config_icon_mask. After the bound is set, the
+ * returned path's computed bound is same as the #getBounds().
+ *
+ * @return the mask path object used to clip the drawable
+ */
+ public Path getIconMask() {
+ return mMask;
+ }
+
+ /**
+ * Returns the foreground drawable managed by this class. The bound of this drawable is
+ * extended by {@link #getExtraInsetFraction()} * getBounds().width on left/right sides and by
+ * {@link #getExtraInsetFraction()} * getBounds().height on top/bottom sides.
+ *
+ * @return the foreground drawable managed by this drawable
+ */
+ public Drawable getForeground() {
+ return mLayerState.mChildren[FOREGROUND_ID].mDrawable;
+ }
+
+ /**
+ * Returns the foreground drawable managed by this class. The bound of this drawable is
+ * extended by {@link #getExtraInsetFraction()} * getBounds().width on left/right sides and by
+ * {@link #getExtraInsetFraction()} * getBounds().height on top/bottom sides.
+ *
+ * @return the background drawable managed by this drawable
+ */
+ public Drawable getBackground() {
+ return mLayerState.mChildren[BACKGROUND_ID].mDrawable;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ if (bounds.isEmpty()) {
+ return;
+ }
+ updateLayerBounds(bounds);
+ }
+
+ private void updateLayerBounds(Rect bounds) {
+ if (bounds.isEmpty()) {
+ return;
+ }
+ try {
+ suspendChildInvalidation();
+ updateLayerBoundsInternal(bounds);
+ updateMaskBoundsInternal(bounds);
+ } finally {
+ resumeChildInvalidation();
+ }
+ }
+
+ /**
+ * Set the child layer bounds bigger than the view port size by {@link #DEFAULT_VIEW_PORT_SCALE}
+ */
+ private void updateLayerBoundsInternal(Rect bounds) {
+ int cX = bounds.width() / 2;
+ int cY = bounds.height() / 2;
+
+ for (int i = 0, count = mLayerState.N_CHILDREN; i < count; i++) {
+ final ChildDrawable r = mLayerState.mChildren[i];
+ if (r == null) {
+ continue;
+ }
+ final Drawable d = r.mDrawable;
+ if (d == null) {
+ continue;
+ }
+
+ int insetWidth = (int) (bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2));
+ int insetHeight = (int) (bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2));
+ final Rect outRect = mTmpOutRect;
+ outRect.set(cX - insetWidth, cY - insetHeight, cX + insetWidth, cY + insetHeight);
+
+ d.setBounds(outRect);
+ }
+ }
+
+ private void updateMaskBoundsInternal(Rect b) {
+ mMaskMatrix.setScale(b.width() / MASK_SIZE, b.height() / MASK_SIZE);
+ sMask.transform(mMaskMatrix, mMask);
+
+ if (mMaskBitmap == null || mMaskBitmap.getWidth() != b.width() ||
+ mMaskBitmap.getHeight() != b.height()) {
+ mMaskBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ALPHA_8);
+ mLayersBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ARGB_8888);
+ }
+ // mMaskBitmap bound [0, w] x [0, h]
+ mCanvas.setBitmap(mMaskBitmap);
+ mPaint.setShader(null);
+ mCanvas.drawPath(mMask, mPaint);
+
+ // mMask bound [left, top, right, bottom]
+ mMaskMatrix.postTranslate(b.left, b.top);
+ mMask.reset();
+ sMask.transform(mMaskMatrix, mMask);
+ // reset everything that depends on the view bounds
+ mTransparentRegion.setEmpty();
+ mLayersShader = null;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mLayersBitmap == null) {
+ return;
+ }
+ if (mLayersShader == null) {
+ mCanvas.setBitmap(mLayersBitmap);
+ mCanvas.drawColor(Color.BLACK);
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ if (mLayerState.mChildren[i] == null) {
+ continue;
+ }
+ final Drawable dr = mLayerState.mChildren[i].mDrawable;
+ if (dr != null) {
+ dr.draw(mCanvas);
+ }
+ }
+ mLayersShader = new BitmapShader(mLayersBitmap, TileMode.CLAMP, TileMode.CLAMP);
+ mPaint.setShader(mLayersShader);
+ }
+ if (mMaskBitmap != null) {
+ Rect bounds = getBounds();
+ canvas.drawBitmap(mMaskBitmap, bounds.left, bounds.top, mPaint);
+ }
+ }
+
+ @Override
+ public void invalidateSelf() {
+ mLayersShader = null;
+ super.invalidateSelf();
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ outline.setConvexPath(mMask);
+ }
+
+ /** @hide */
+ @TestApi
+ public Region getSafeZone() {
+ mMaskMatrix.reset();
+ mMaskMatrix.setScale(SAFEZONE_SCALE, SAFEZONE_SCALE, getBounds().centerX(), getBounds().centerY());
+ Path p = new Path();
+ mMask.transform(mMaskMatrix, p);
+ Region safezoneRegion = new Region(getBounds());
+ safezoneRegion.setPath(p, safezoneRegion);
+ return safezoneRegion;
+ }
+
+ @Override
+ public @Nullable Region getTransparentRegion() {
+ if (mTransparentRegion.isEmpty()) {
+ mMask.toggleInverseFillType();
+ mTransparentRegion.set(getBounds());
+ mTransparentRegion.setPath(mMask, mTransparentRegion);
+ mMask.toggleInverseFillType();
+ }
+ return mTransparentRegion;
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ final LayerState state = mLayerState;
+ if (state == null) {
+ return;
+ }
+
+ final int density = Drawable.resolveDensity(t.getResources(), 0);
+ state.setDensity(density);
+
+ final ChildDrawable[] array = state.mChildren;
+ for (int i = 0; i < state.N_CHILDREN; i++) {
+ final ChildDrawable layer = array[i];
+ layer.setDensity(density);
+
+ if (layer.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(
+ layer.mThemeAttrs, R.styleable.AdaptiveIconDrawableLayer);
+ updateLayerFromTypedArray(layer, a);
+ a.recycle();
+ }
+
+ final Drawable d = layer.mDrawable;
+ if (d != null && d.canApplyTheme()) {
+ d.applyTheme(t);
+
+ // Update cached mask of child changing configurations.
+ state.mChildrenChangingConfigurations |= d.getChangingConfigurations();
+ }
+ }
+ }
+
+ /**
+ * Inflates child layers using the specified parser.
+ */
+ private void inflateLayers(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ final LayerState state = mLayerState;
+
+ final int innerDepth = parser.getDepth() + 1;
+ int type;
+ int depth;
+ int childIndex = 0;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (depth > innerDepth) {
+ continue;
+ }
+ String tagName = parser.getName();
+ if (tagName.equals("background")) {
+ childIndex = BACKGROUND_ID;
+ } else if (tagName.equals("foreground")) {
+ childIndex = FOREGROUND_ID;
+ } else {
+ continue;
+ }
+
+ final ChildDrawable layer = new ChildDrawable(state.mDensity);
+ final TypedArray a = obtainAttributes(r, theme, attrs,
+ R.styleable.AdaptiveIconDrawableLayer);
+ updateLayerFromTypedArray(layer, a);
+ a.recycle();
+
+ // If the layer doesn't have a drawable or unresolved theme
+ // attribute for a drawable, attempt to parse one from the child
+ // element. If multiple child elements exist, we'll only use the
+ // first one.
+ if (layer.mDrawable == null && (layer.mThemeAttrs == null)) {
+ while ((type = parser.next()) == XmlPullParser.TEXT) {
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException(parser.getPositionDescription()
+ + ": <foreground> or <background> tag requires a 'drawable'"
+ + "attribute or child tag defining a drawable");
+ }
+
+ // We found a child drawable. Take ownership.
+ layer.mDrawable = Drawable.createFromXmlInnerForDensity(r, parser, attrs,
+ mLayerState.mSrcDensityOverride, theme);
+ layer.mDrawable.setCallback(this);
+ state.mChildrenChangingConfigurations |=
+ layer.mDrawable.getChangingConfigurations();
+ }
+ addLayer(childIndex, layer);
+ }
+ }
+
+ private void updateLayerFromTypedArray(@NonNull ChildDrawable layer, @NonNull TypedArray a) {
+ final LayerState state = mLayerState;
+
+ // Account for any configuration changes.
+ state.mChildrenChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ layer.mThemeAttrs = a.extractThemeAttrs();
+
+ Drawable dr = a.getDrawableForDensity(R.styleable.AdaptiveIconDrawableLayer_drawable,
+ state.mSrcDensityOverride);
+ if (dr != null) {
+ if (layer.mDrawable != null) {
+ // It's possible that a drawable was already set, in which case
+ // we should clear the callback. We may have also integrated the
+ // drawable's changing configurations, but we don't have enough
+ // information to revert that change.
+ layer.mDrawable.setCallback(null);
+ }
+
+ // Take ownership of the new drawable.
+ layer.mDrawable = dr;
+ layer.mDrawable.setCallback(this);
+ state.mChildrenChangingConfigurations |=
+ layer.mDrawable.getChangingConfigurations();
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return (mLayerState != null && mLayerState.canApplyTheme()) || super.canApplyTheme();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean isProjected() {
+ if (super.isProjected()) {
+ return true;
+ }
+
+ final ChildDrawable[] layers = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ if (layers[i].mDrawable.isProjected()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Temporarily suspends child invalidation.
+ *
+ * @see #resumeChildInvalidation()
+ */
+ private void suspendChildInvalidation() {
+ mSuspendChildInvalidation = true;
+ }
+
+ /**
+ * Resumes child invalidation after suspension, immediately performing an
+ * invalidation if one was requested by a child during suspension.
+ *
+ * @see #suspendChildInvalidation()
+ */
+ private void resumeChildInvalidation() {
+ mSuspendChildInvalidation = false;
+
+ if (mChildRequestedInvalidation) {
+ mChildRequestedInvalidation = false;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable who) {
+ if (mSuspendChildInvalidation) {
+ mChildRequestedInvalidation = true;
+ } else {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+ scheduleSelf(what, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+ unscheduleSelf(what);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations() | mLayerState.getChangingConfigurations();
+ }
+
+ @Override
+ public void setHotspot(float x, float y) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setHotspot(x, y);
+ }
+ }
+ }
+
+ @Override
+ public void setHotspotBounds(int left, int top, int right, int bottom) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setHotspotBounds(left, top, right, bottom);
+ }
+ }
+
+ if (mHotspotBounds == null) {
+ mHotspotBounds = new Rect(left, top, right, bottom);
+ } else {
+ mHotspotBounds.set(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public void getHotspotBounds(Rect outRect) {
+ if (mHotspotBounds != null) {
+ outRect.set(mHotspotBounds);
+ } else {
+ super.getHotspotBounds(outRect);
+ }
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ final boolean changed = super.setVisible(visible, restart);
+ final ChildDrawable[] array = mLayerState.mChildren;
+
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setVisible(visible, restart);
+ }
+ }
+
+ return changed;
+ }
+
+ @Override
+ public void setDither(boolean dither) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setDither(dither);
+ }
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setAlpha(alpha);
+ }
+ }
+ }
+
+ @Override
+ public int getAlpha() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setColorFilter(colorFilter);
+ }
+ }
+ }
+
+ @Override
+ public void setTintList(ColorStateList tint) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.N_CHILDREN;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setTintList(tint);
+ }
+ }
+ }
+
+ @Override
+ public void setTintMode(Mode tintMode) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.N_CHILDREN;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setTintMode(tintMode);
+ }
+ }
+ }
+
+ public void setOpacity(int opacity) {
+ mLayerState.mOpacityOverride = opacity;
+ }
+
+ @Override
+ public int getOpacity() {
+ if (mLayerState.mOpacityOverride != PixelFormat.UNKNOWN) {
+ return mLayerState.mOpacityOverride;
+ }
+ return mLayerState.getOpacity();
+ }
+
+ @Override
+ public void setAutoMirrored(boolean mirrored) {
+ mLayerState.mAutoMirrored = mirrored;
+
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setAutoMirrored(mirrored);
+ }
+ }
+ }
+
+ @Override
+ public boolean isAutoMirrored() {
+ return mLayerState.mAutoMirrored;
+ }
+
+ @Override
+ public void jumpToCurrentState() {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.jumpToCurrentState();
+ }
+ }
+ }
+
+ @Override
+ public boolean isStateful() {
+ return mLayerState.isStateful();
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return mLayerState.hasFocusStateSpecified();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ boolean changed = false;
+
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null && dr.isStateful() && dr.setState(state)) {
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ updateLayerBounds(getBounds());
+ }
+
+ return changed;
+ }
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ boolean changed = false;
+
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null && dr.setLevel(level)) {
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ updateLayerBounds(getBounds());
+ }
+
+ return changed;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return (int)(getMaxIntrinsicWidth() * DEFAULT_VIEW_PORT_SCALE);
+ }
+
+ private int getMaxIntrinsicWidth() {
+ int width = -1;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final ChildDrawable r = mLayerState.mChildren[i];
+ if (r.mDrawable == null) {
+ continue;
+ }
+ final int w = r.mDrawable.getIntrinsicWidth();
+ if (w > width) {
+ width = w;
+ }
+ }
+ return width;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return (int)(getMaxIntrinsicHeight() * DEFAULT_VIEW_PORT_SCALE);
+ }
+
+ private int getMaxIntrinsicHeight() {
+ int height = -1;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final ChildDrawable r = mLayerState.mChildren[i];
+ if (r.mDrawable == null) {
+ continue;
+ }
+ final int h = r.mDrawable.getIntrinsicHeight();
+ if (h > height) {
+ height = h;
+ }
+ }
+ return height;
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ if (mLayerState.canConstantState()) {
+ mLayerState.mChangingConfigurations = getChangingConfigurations();
+ return mLayerState;
+ }
+ return null;
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mLayerState = createConstantState(mLayerState, null);
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = mLayerState.mChildren[i].mDrawable;
+ if (dr != null) {
+ dr.mutate();
+ }
+ }
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.clearMutated();
+ }
+ }
+ mMutated = false;
+ }
+
+ static class ChildDrawable {
+ public Drawable mDrawable;
+ public int[] mThemeAttrs;
+ public int mDensity = DisplayMetrics.DENSITY_DEFAULT;
+
+ ChildDrawable(int density) {
+ mDensity = density;
+ }
+
+ ChildDrawable(@NonNull ChildDrawable orig, @NonNull AdaptiveIconDrawable owner,
+ @Nullable Resources res) {
+
+ final Drawable dr = orig.mDrawable;
+ final Drawable clone;
+ if (dr != null) {
+ final ConstantState cs = dr.getConstantState();
+ if (cs == null) {
+ clone = dr;
+ } else if (res != null) {
+ clone = cs.newDrawable(res);
+ } else {
+ clone = cs.newDrawable();
+ }
+ clone.setCallback(owner);
+ clone.setBounds(dr.getBounds());
+ clone.setLevel(dr.getLevel());
+ } else {
+ clone = null;
+ }
+
+ mDrawable = clone;
+ mThemeAttrs = orig.mThemeAttrs;
+
+ mDensity = Drawable.resolveDensity(res, orig.mDensity);
+ }
+
+ public boolean canApplyTheme() {
+ return mThemeAttrs != null
+ || (mDrawable != null && mDrawable.canApplyTheme());
+ }
+
+ public final void setDensity(int targetDensity) {
+ if (mDensity != targetDensity) {
+ mDensity = targetDensity;
+ }
+ }
+ }
+
+ static class LayerState extends ConstantState {
+ private int[] mThemeAttrs;
+
+ final static int N_CHILDREN = 2;
+ ChildDrawable[] mChildren;
+
+ // The density at which to render the drawable and its children.
+ int mDensity;
+
+ // The density to use when inflating/looking up the children drawables. A value of 0 means
+ // use the system's density.
+ int mSrcDensityOverride = 0;
+
+ int mOpacityOverride = PixelFormat.UNKNOWN;
+
+ @Config int mChangingConfigurations;
+ @Config int mChildrenChangingConfigurations;
+
+ private boolean mCheckedOpacity;
+ private int mOpacity;
+
+ private boolean mCheckedStateful;
+ private boolean mIsStateful;
+ private boolean mAutoMirrored = false;
+
+ LayerState(@Nullable LayerState orig, @NonNull AdaptiveIconDrawable owner,
+ @Nullable Resources res) {
+ mDensity = Drawable.resolveDensity(res, orig != null ? orig.mDensity : 0);
+ mChildren = new ChildDrawable[N_CHILDREN];
+ if (orig != null) {
+ final ChildDrawable[] origChildDrawable = orig.mChildren;
+
+ mChangingConfigurations = orig.mChangingConfigurations;
+ mChildrenChangingConfigurations = orig.mChildrenChangingConfigurations;
+
+ for (int i = 0; i < N_CHILDREN; i++) {
+ final ChildDrawable or = origChildDrawable[i];
+ mChildren[i] = new ChildDrawable(or, owner, res);
+ }
+
+ mCheckedOpacity = orig.mCheckedOpacity;
+ mOpacity = orig.mOpacity;
+ mCheckedStateful = orig.mCheckedStateful;
+ mIsStateful = orig.mIsStateful;
+ mAutoMirrored = orig.mAutoMirrored;
+ mThemeAttrs = orig.mThemeAttrs;
+ mOpacityOverride = orig.mOpacityOverride;
+ mSrcDensityOverride = orig.mSrcDensityOverride;
+ } else {
+ for (int i = 0; i < N_CHILDREN; i++) {
+ mChildren[i] = new ChildDrawable(mDensity);
+ }
+ }
+ }
+
+ public final void setDensity(int targetDensity) {
+ if (mDensity != targetDensity) {
+ mDensity = targetDensity;
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ if (mThemeAttrs != null || super.canApplyTheme()) {
+ return true;
+ }
+
+ final ChildDrawable[] array = mChildren;
+ for (int i = 0; i < N_CHILDREN; i++) {
+ final ChildDrawable layer = array[i];
+ if (layer.canApplyTheme()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new AdaptiveIconDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(@Nullable Resources res) {
+ return new AdaptiveIconDrawable(this, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations
+ | mChildrenChangingConfigurations;
+ }
+
+ public final int getOpacity() {
+ if (mCheckedOpacity) {
+ return mOpacity;
+ }
+
+ final ChildDrawable[] array = mChildren;
+
+ // Seek to the first non-null drawable.
+ int firstIndex = -1;
+ for (int i = 0; i < N_CHILDREN; i++) {
+ if (array[i].mDrawable != null) {
+ firstIndex = i;
+ break;
+ }
+ }
+
+ int op;
+ if (firstIndex >= 0) {
+ op = array[firstIndex].mDrawable.getOpacity();
+ } else {
+ op = PixelFormat.TRANSPARENT;
+ }
+
+ // Merge all remaining non-null drawables.
+ for (int i = firstIndex + 1; i < N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ op = Drawable.resolveOpacity(op, dr.getOpacity());
+ }
+ }
+
+ mOpacity = op;
+ mCheckedOpacity = true;
+ return op;
+ }
+
+ public final boolean isStateful() {
+ if (mCheckedStateful) {
+ return mIsStateful;
+ }
+
+ final ChildDrawable[] array = mChildren;
+ boolean isStateful = false;
+ for (int i = 0; i < N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null && dr.isStateful()) {
+ isStateful = true;
+ break;
+ }
+ }
+
+ mIsStateful = isStateful;
+ mCheckedStateful = true;
+ return isStateful;
+ }
+
+ public final boolean hasFocusStateSpecified() {
+ final ChildDrawable[] array = mChildren;
+ for (int i = 0; i < N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null && dr.hasFocusStateSpecified()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public final boolean canConstantState() {
+ final ChildDrawable[] array = mChildren;
+ for (int i = 0; i < N_CHILDREN; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null && dr.getConstantState() == null) {
+ return false;
+ }
+ }
+
+ // Don't cache the result, this method is not called very often.
+ return true;
+ }
+
+ public void invalidateCache() {
+ mCheckedOpacity = false;
+ mCheckedStateful = false;
+ }
+ }
+}
diff --git a/android/graphics/drawable/AdaptiveIconDrawable_Delegate.java b/android/graphics/drawable/AdaptiveIconDrawable_Delegate.java
new file mode 100644
index 0000000..ca6bc85
--- /dev/null
+++ b/android/graphics/drawable/AdaptiveIconDrawable_Delegate.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 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 android.graphics.drawable;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.content.res.Resources;
+import android.content.res.Resources_Delegate;
+import android.util.PathParser;
+
+import static com.android.layoutlib.bridge.android.RenderParamsFlags.FLAG_KEY_ADAPTIVE_ICON_MASK_PATH;
+
+public class AdaptiveIconDrawable_Delegate {
+
+ @LayoutlibDelegate
+ /*package*/ static void constructor_after(AdaptiveIconDrawable icon) {
+ String pathString = Resources_Delegate.getLayoutlibCallback(Resources.getSystem()).getFlag(
+ FLAG_KEY_ADAPTIVE_ICON_MASK_PATH);
+ if (pathString != null) {
+ AdaptiveIconDrawable.sMask = PathParser.createPathFromPathData(pathString);
+ }
+ }
+}
diff --git a/android/graphics/drawable/Animatable.java b/android/graphics/drawable/Animatable.java
new file mode 100644
index 0000000..983d650
--- /dev/null
+++ b/android/graphics/drawable/Animatable.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2009 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 android.graphics.drawable;
+
+/**
+ * Interface that drawables supporting animations should implement.
+ */
+public interface Animatable {
+ /**
+ * Starts the drawable's animation.
+ */
+ void start();
+
+ /**
+ * Stops the drawable's animation.
+ */
+ void stop();
+
+ /**
+ * Indicates whether the animation is running.
+ *
+ * @return True if the animation is running, false otherwise.
+ */
+ boolean isRunning();
+}
diff --git a/android/graphics/drawable/Animatable2.java b/android/graphics/drawable/Animatable2.java
new file mode 100644
index 0000000..7c7e60e
--- /dev/null
+++ b/android/graphics/drawable/Animatable2.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015 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 android.graphics.drawable;
+
+import android.annotation.NonNull;
+
+/**
+ * Abstract class that drawables supporting animations and callbacks should extend.
+ */
+public interface Animatable2 extends Animatable {
+
+ /**
+ * Adds a callback to listen to the animation events.
+ *
+ * @param callback Callback to add.
+ */
+ void registerAnimationCallback(@NonNull AnimationCallback callback);
+
+ /**
+ * Removes the specified animation callback.
+ *
+ * @param callback Callback to remove.
+ * @return {@code false} if callback didn't exist in the call back list, or {@code true} if
+ * callback has been removed successfully.
+ */
+ boolean unregisterAnimationCallback(@NonNull AnimationCallback callback);
+
+ /**
+ * Removes all existing animation callbacks.
+ */
+ void clearAnimationCallbacks();
+
+ public static abstract class AnimationCallback {
+ /**
+ * Called when the animation starts.
+ *
+ * @param drawable The drawable started the animation.
+ */
+ public void onAnimationStart(Drawable drawable) {};
+ /**
+ * Called when the animation ends.
+ *
+ * @param drawable The drawable finished the animation.
+ */
+ public void onAnimationEnd(Drawable drawable) {};
+ }
+}
diff --git a/android/graphics/drawable/AnimatedRotateDrawable.java b/android/graphics/drawable/AnimatedRotateDrawable.java
new file mode 100644
index 0000000..d714ca8
--- /dev/null
+++ b/android/graphics/drawable/AnimatedRotateDrawable.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2009 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 android.graphics.drawable;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.Resources.Theme;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.os.SystemClock;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+import com.android.internal.R;
+
+/**
+ * @hide
+ */
+public class AnimatedRotateDrawable extends DrawableWrapper implements Animatable {
+ private AnimatedRotateState mState;
+
+ private float mCurrentDegrees;
+ private float mIncrement;
+
+ /** Whether this drawable is currently animating. */
+ private boolean mRunning;
+
+ /**
+ * Creates a new animated rotating drawable with no wrapped drawable.
+ */
+ public AnimatedRotateDrawable() {
+ this(new AnimatedRotateState(null, null), null);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Drawable drawable = getDrawable();
+ final Rect bounds = drawable.getBounds();
+ final int w = bounds.right - bounds.left;
+ final int h = bounds.bottom - bounds.top;
+
+ final AnimatedRotateState st = mState;
+ final float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX;
+ final float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY;
+
+ final int saveCount = canvas.save();
+ canvas.rotate(mCurrentDegrees, px + bounds.left, py + bounds.top);
+ drawable.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+
+ /**
+ * Starts the rotation animation.
+ * <p>
+ * The animation will run until {@link #stop()} is called. Calling this
+ * method while the animation is already running has no effect.
+ *
+ * @see #stop()
+ */
+ @Override
+ public void start() {
+ if (!mRunning) {
+ mRunning = true;
+ nextFrame();
+ }
+ }
+
+ /**
+ * Stops the rotation animation.
+ *
+ * @see #start()
+ */
+ @Override
+ public void stop() {
+ mRunning = false;
+ unscheduleSelf(mNextFrame);
+ }
+
+ @Override
+ public boolean isRunning() {
+ return mRunning;
+ }
+
+ private void nextFrame() {
+ unscheduleSelf(mNextFrame);
+ scheduleSelf(mNextFrame, SystemClock.uptimeMillis() + mState.mFrameDuration);
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ final boolean changed = super.setVisible(visible, restart);
+ if (visible) {
+ if (changed || restart) {
+ mCurrentDegrees = 0.0f;
+ nextFrame();
+ }
+ } else {
+ unscheduleSelf(mNextFrame);
+ }
+ return changed;
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedRotateDrawable);
+
+ // Inflation will advance the XmlPullParser and AttributeSet.
+ super.inflate(r, parser, attrs, theme);
+
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ a.recycle();
+
+ updateLocalState();
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ final AnimatedRotateState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(
+ state.mThemeAttrs, R.styleable.AnimatedRotateDrawable);
+ try {
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ } catch (XmlPullParserException e) {
+ rethrowAsRuntimeException(e);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ updateLocalState();
+ }
+
+ private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
+ // If we're not waiting on a theme, verify required attributes.
+ if (getDrawable() == null && (mState.mThemeAttrs == null
+ || mState.mThemeAttrs[R.styleable.AnimatedRotateDrawable_drawable] == 0)) {
+ throw new XmlPullParserException(a.getPositionDescription()
+ + ": <animated-rotate> tag requires a 'drawable' attribute or "
+ + "child tag defining a drawable");
+ }
+ }
+
+ private void updateStateFromTypedArray(@NonNull TypedArray a) {
+ final AnimatedRotateState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotX)) {
+ final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotX);
+ state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION;
+ state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
+ }
+
+ if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotY)) {
+ final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotY);
+ state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION;
+ state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
+ }
+
+ setFramesCount(a.getInt(
+ R.styleable.AnimatedRotateDrawable_framesCount, state.mFramesCount));
+ setFramesDuration(a.getInt(
+ R.styleable.AnimatedRotateDrawable_frameDuration, state.mFrameDuration));
+ }
+
+ public void setFramesCount(int framesCount) {
+ mState.mFramesCount = framesCount;
+ mIncrement = 360.0f / mState.mFramesCount;
+ }
+
+ public void setFramesDuration(int framesDuration) {
+ mState.mFrameDuration = framesDuration;
+ }
+
+ @Override
+ DrawableWrapperState mutateConstantState() {
+ mState = new AnimatedRotateState(mState, null);
+ return mState;
+ }
+
+ static final class AnimatedRotateState extends DrawableWrapper.DrawableWrapperState {
+ private int[] mThemeAttrs;
+
+ boolean mPivotXRel = false;
+ float mPivotX = 0;
+ boolean mPivotYRel = false;
+ float mPivotY = 0;
+ int mFrameDuration = 150;
+ int mFramesCount = 12;
+
+ public AnimatedRotateState(AnimatedRotateState orig, Resources res) {
+ super(orig, res);
+
+ if (orig != null) {
+ mPivotXRel = orig.mPivotXRel;
+ mPivotX = orig.mPivotX;
+ mPivotYRel = orig.mPivotYRel;
+ mPivotY = orig.mPivotY;
+ mFramesCount = orig.mFramesCount;
+ mFrameDuration = orig.mFrameDuration;
+ }
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new AnimatedRotateDrawable(this, res);
+ }
+ }
+
+ private AnimatedRotateDrawable(AnimatedRotateState state, Resources res) {
+ super(state, res);
+
+ mState = state;
+
+ updateLocalState();
+ }
+
+ private void updateLocalState() {
+ final AnimatedRotateState state = mState;
+ mIncrement = 360.0f / state.mFramesCount;
+
+ // Force the wrapped drawable to use filtering and AA, if applicable,
+ // so that it looks smooth when rotated.
+ final Drawable drawable = getDrawable();
+ if (drawable != null) {
+ drawable.setFilterBitmap(true);
+ if (drawable instanceof BitmapDrawable) {
+ ((BitmapDrawable) drawable).setAntiAlias(true);
+ }
+ }
+ }
+
+ private final Runnable mNextFrame = new Runnable() {
+ @Override
+ public void run() {
+ // TODO: This should be computed in draw(Canvas), based on the amount
+ // of time since the last frame drawn
+ mCurrentDegrees += mIncrement;
+ if (mCurrentDegrees > (360.0f - mIncrement)) {
+ mCurrentDegrees = 0.0f;
+ }
+ invalidateSelf();
+ nextFrame();
+ }
+ };
+}
diff --git a/android/graphics/drawable/AnimatedStateListDrawable.java b/android/graphics/drawable/AnimatedStateListDrawable.java
new file mode 100644
index 0000000..3ed6a78
--- /dev/null
+++ b/android/graphics/drawable/AnimatedStateListDrawable.java
@@ -0,0 +1,735 @@
+/*
+ * Copyright (C) 2014 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 android.graphics.drawable;
+
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.LongSparseLongArray;
+import android.util.SparseIntArray;
+import android.util.StateSet;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Drawable containing a set of Drawable keyframes where the currently displayed
+ * keyframe is chosen based on the current state set. Animations between
+ * keyframes may optionally be defined using transition elements.
+ * <p>
+ * This drawable can be defined in an XML file with the <code>
+ * <animated-selector></code> element. Each keyframe Drawable is defined in a
+ * nested <code><item></code> element. Transitions are defined in a nested
+ * <code><transition></code> element.
+ *
+ * @attr ref android.R.styleable#DrawableStates_state_focused
+ * @attr ref android.R.styleable#DrawableStates_state_window_focused
+ * @attr ref android.R.styleable#DrawableStates_state_enabled
+ * @attr ref android.R.styleable#DrawableStates_state_checkable
+ * @attr ref android.R.styleable#DrawableStates_state_checked
+ * @attr ref android.R.styleable#DrawableStates_state_selected
+ * @attr ref android.R.styleable#DrawableStates_state_activated
+ * @attr ref android.R.styleable#DrawableStates_state_active
+ * @attr ref android.R.styleable#DrawableStates_state_single
+ * @attr ref android.R.styleable#DrawableStates_state_first
+ * @attr ref android.R.styleable#DrawableStates_state_middle
+ * @attr ref android.R.styleable#DrawableStates_state_last
+ * @attr ref android.R.styleable#DrawableStates_state_pressed
+ */
+public class AnimatedStateListDrawable extends StateListDrawable {
+ private static final String LOGTAG = AnimatedStateListDrawable.class.getSimpleName();
+
+ private static final String ELEMENT_TRANSITION = "transition";
+ private static final String ELEMENT_ITEM = "item";
+
+ private AnimatedStateListState mState;
+
+ /** The currently running transition, if any. */
+ private Transition mTransition;
+
+ /** Index to be set after the transition ends. */
+ private int mTransitionToIndex = -1;
+
+ /** Index away from which we are transitioning. */
+ private int mTransitionFromIndex = -1;
+
+ private boolean mMutated;
+
+ public AnimatedStateListDrawable() {
+ this(null, null);
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ final boolean changed = super.setVisible(visible, restart);
+
+ if (mTransition != null && (changed || restart)) {
+ if (visible) {
+ mTransition.start();
+ } else {
+ // Ensure we're showing the correct state when visible.
+ jumpToCurrentState();
+ }
+ }
+
+ return changed;
+ }
+
+ /**
+ * Add a new drawable to the set of keyframes.
+ *
+ * @param stateSet An array of resource IDs to associate with the keyframe
+ * @param drawable The drawable to show when in the specified state, may not be null
+ * @param id The unique identifier for the keyframe
+ */
+ public void addState(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
+ if (drawable == null) {
+ throw new IllegalArgumentException("Drawable must not be null");
+ }
+
+ mState.addStateSet(stateSet, drawable, id);
+ onStateChange(getState());
+ }
+
+ /**
+ * Adds a new transition between keyframes.
+ *
+ * @param fromId Unique identifier of the starting keyframe
+ * @param toId Unique identifier of the ending keyframe
+ * @param transition An {@link Animatable} drawable to use as a transition, may not be null
+ * @param reversible Whether the transition can be reversed
+ */
+ public <T extends Drawable & Animatable> void addTransition(int fromId, int toId,
+ @NonNull T transition, boolean reversible) {
+ if (transition == null) {
+ throw new IllegalArgumentException("Transition drawable must not be null");
+ }
+
+ mState.addTransition(fromId, toId, transition, reversible);
+ }
+
+ @Override
+ public boolean isStateful() {
+ return true;
+ }
+
+ @Override
+ protected boolean onStateChange(int[] stateSet) {
+ // If we're not already at the target index, either attempt to find a
+ // valid transition to it or jump directly there.
+ final int targetIndex = mState.indexOfKeyframe(stateSet);
+ boolean changed = targetIndex != getCurrentIndex()
+ && (selectTransition(targetIndex) || selectDrawable(targetIndex));
+
+ // We need to propagate the state change to the current drawable, but
+ // we can't call StateListDrawable.onStateChange() without changing the
+ // current drawable.
+ final Drawable current = getCurrent();
+ if (current != null) {
+ changed |= current.setState(stateSet);
+ }
+
+ return changed;
+ }
+
+ private boolean selectTransition(int toIndex) {
+ final int fromIndex;
+ final Transition currentTransition = mTransition;
+ if (currentTransition != null) {
+ if (toIndex == mTransitionToIndex) {
+ // Already animating to that keyframe.
+ return true;
+ } else if (toIndex == mTransitionFromIndex && currentTransition.canReverse()) {
+ // Reverse the current animation.
+ currentTransition.reverse();
+ mTransitionToIndex = mTransitionFromIndex;
+ mTransitionFromIndex = toIndex;
+ return true;
+ }
+
+ // Start the next transition from the end of the current one.
+ fromIndex = mTransitionToIndex;
+
+ // Changing animation, end the current animation.
+ currentTransition.stop();
+ } else {
+ fromIndex = getCurrentIndex();
+ }
+
+ // Reset state.
+ mTransition = null;
+ mTransitionFromIndex = -1;
+ mTransitionToIndex = -1;
+
+ final AnimatedStateListState state = mState;
+ final int fromId = state.getKeyframeIdAt(fromIndex);
+ final int toId = state.getKeyframeIdAt(toIndex);
+ if (toId == 0 || fromId == 0) {
+ // Missing a keyframe ID.
+ return false;
+ }
+
+ final int transitionIndex = state.indexOfTransition(fromId, toId);
+ if (transitionIndex < 0) {
+ // Couldn't select a transition.
+ return false;
+ }
+
+ boolean hasReversibleFlag = state.transitionHasReversibleFlag(fromId, toId);
+
+ // This may fail if we're already on the transition, but that's okay!
+ selectDrawable(transitionIndex);
+
+ final Transition transition;
+ final Drawable d = getCurrent();
+ if (d instanceof AnimationDrawable) {
+ final boolean reversed = state.isTransitionReversed(fromId, toId);
+
+ transition = new AnimationDrawableTransition((AnimationDrawable) d,
+ reversed, hasReversibleFlag);
+ } else if (d instanceof AnimatedVectorDrawable) {
+ final boolean reversed = state.isTransitionReversed(fromId, toId);
+
+ transition = new AnimatedVectorDrawableTransition((AnimatedVectorDrawable) d,
+ reversed, hasReversibleFlag);
+ } else if (d instanceof Animatable) {
+ transition = new AnimatableTransition((Animatable) d);
+ } else {
+ // We don't know how to animate this transition.
+ return false;
+ }
+
+ transition.start();
+
+ mTransition = transition;
+ mTransitionFromIndex = fromIndex;
+ mTransitionToIndex = toIndex;
+ return true;
+ }
+
+ private static abstract class Transition {
+ public abstract void start();
+ public abstract void stop();
+
+ public void reverse() {
+ // Not supported by default.
+ }
+
+ public boolean canReverse() {
+ return false;
+ }
+ }
+
+ private static class AnimatableTransition extends Transition {
+ private final Animatable mA;
+
+ public AnimatableTransition(Animatable a) {
+ mA = a;
+ }
+
+ @Override
+ public void start() {
+ mA.start();
+ }
+
+ @Override
+ public void stop() {
+ mA.stop();
+ }
+ }
+
+
+ private static class AnimationDrawableTransition extends Transition {
+ private final ObjectAnimator mAnim;
+
+ // Even AnimationDrawable is always reversible technically, but
+ // we should obey the XML's android:reversible flag.
+ private final boolean mHasReversibleFlag;
+
+ public AnimationDrawableTransition(AnimationDrawable ad,
+ boolean reversed, boolean hasReversibleFlag) {
+ final int frameCount = ad.getNumberOfFrames();
+ final int fromFrame = reversed ? frameCount - 1 : 0;
+ final int toFrame = reversed ? 0 : frameCount - 1;
+ final FrameInterpolator interp = new FrameInterpolator(ad, reversed);
+ final ObjectAnimator anim = ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame);
+ anim.setAutoCancel(true);
+ anim.setDuration(interp.getTotalDuration());
+ anim.setInterpolator(interp);
+ mHasReversibleFlag = hasReversibleFlag;
+ mAnim = anim;
+ }
+
+ @Override
+ public boolean canReverse() {
+ return mHasReversibleFlag;
+ }
+
+ @Override
+ public void start() {
+ mAnim.start();
+ }
+
+ @Override
+ public void reverse() {
+ mAnim.reverse();
+ }
+
+ @Override
+ public void stop() {
+ mAnim.cancel();
+ }
+ }
+
+ private static class AnimatedVectorDrawableTransition extends Transition {
+ private final AnimatedVectorDrawable mAvd;
+
+ // mReversed is indicating the current transition's direction.
+ private final boolean mReversed;
+
+ // mHasReversibleFlag is indicating whether the whole transition has
+ // reversible flag set to true.
+ // If mHasReversibleFlag is false, then mReversed is always false.
+ private final boolean mHasReversibleFlag;
+
+ public AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd,
+ boolean reversed, boolean hasReversibleFlag) {
+ mAvd = avd;
+ mReversed = reversed;
+ mHasReversibleFlag = hasReversibleFlag;
+ }
+
+ @Override
+ public boolean canReverse() {
+ // When the transition's XML says it is not reversible, then we obey
+ // it, even if the AVD itself is reversible.
+ // This will help the single direction transition.
+ return mAvd.canReverse() && mHasReversibleFlag;
+ }
+
+ @Override
+ public void start() {
+ if (mReversed) {
+ reverse();
+ } else {
+ mAvd.start();
+ }
+ }
+
+ @Override
+ public void reverse() {
+ if (canReverse()) {
+ mAvd.reverse();
+ } else {
+ Log.w(LOGTAG, "Can't reverse, either the reversible is set to false,"
+ + " or the AnimatedVectorDrawable can't reverse");
+ }
+ }
+
+ @Override
+ public void stop() {
+ mAvd.stop();
+ }
+ }
+
+
+ @Override
+ public void jumpToCurrentState() {
+ super.jumpToCurrentState();
+
+ if (mTransition != null) {
+ mTransition.stop();
+ mTransition = null;
+
+ selectDrawable(mTransitionToIndex);
+ mTransitionToIndex = -1;
+ mTransitionFromIndex = -1;
+ }
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = obtainAttributes(
+ r, theme, attrs, R.styleable.AnimatedStateListDrawable);
+ super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible);
+ updateStateFromTypedArray(a);
+ updateDensity(r);
+ a.recycle();
+
+ inflateChildElements(r, parser, attrs, theme);
+
+ init();
+ }
+
+ @Override
+ public void applyTheme(@Nullable Theme theme) {
+ super.applyTheme(theme);
+
+ final AnimatedStateListState state = mState;
+ if (state == null || state.mAnimThemeAttrs == null) {
+ return;
+ }
+
+ final TypedArray a = theme.resolveAttributes(
+ state.mAnimThemeAttrs, R.styleable.AnimatedRotateDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+
+ init();
+ }
+
+ private void updateStateFromTypedArray(TypedArray a) {
+ final AnimatedStateListState state = mState;
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mAnimThemeAttrs = a.extractThemeAttrs();
+
+ state.setVariablePadding(a.getBoolean(
+ R.styleable.AnimatedStateListDrawable_variablePadding, state.mVariablePadding));
+ state.setConstantSize(a.getBoolean(
+ R.styleable.AnimatedStateListDrawable_constantSize, state.mConstantSize));
+ state.setEnterFadeDuration(a.getInt(
+ R.styleable.AnimatedStateListDrawable_enterFadeDuration, state.mEnterFadeDuration));
+ state.setExitFadeDuration(a.getInt(
+ R.styleable.AnimatedStateListDrawable_exitFadeDuration, state.mExitFadeDuration));
+
+ setDither(a.getBoolean(
+ R.styleable.AnimatedStateListDrawable_dither, state.mDither));
+ setAutoMirrored(a.getBoolean(
+ R.styleable.AnimatedStateListDrawable_autoMirrored, state.mAutoMirrored));
+ }
+
+ private void init() {
+ onStateChange(getState());
+ }
+
+ private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
+ Theme theme) throws XmlPullParserException, IOException {
+ int type;
+
+ final int innerDepth = parser.getDepth() + 1;
+ int depth;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth
+ || type != XmlPullParser.END_TAG)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (depth > innerDepth) {
+ continue;
+ }
+
+ if (parser.getName().equals(ELEMENT_ITEM)) {
+ parseItem(r, parser, attrs, theme);
+ } else if (parser.getName().equals(ELEMENT_TRANSITION)) {
+ parseTransition(r, parser, attrs, theme);
+ }
+ }
+ }
+
+ private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ // This allows state list drawable item elements to be themed at
+ // inflation time but does NOT make them work for Zygote preload.
+ final TypedArray a = obtainAttributes(r, theme, attrs,
+ R.styleable.AnimatedStateListDrawableTransition);
+ final int fromId = a.getResourceId(
+ R.styleable.AnimatedStateListDrawableTransition_fromId, 0);
+ final int toId = a.getResourceId(
+ R.styleable.AnimatedStateListDrawableTransition_toId, 0);
+ final boolean reversible = a.getBoolean(
+ R.styleable.AnimatedStateListDrawableTransition_reversible, false);
+ Drawable dr = a.getDrawable(
+ R.styleable.AnimatedStateListDrawableTransition_drawable);
+ a.recycle();
+
+ // Loading child elements modifies the state of the AttributeSet's
+ // underlying parser, so it needs to happen after obtaining
+ // attributes and extracting states.
+ if (dr == null) {
+ int type;
+ while ((type = parser.next()) == XmlPullParser.TEXT) {
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException(
+ parser.getPositionDescription()
+ + ": <transition> tag requires a 'drawable' attribute or "
+ + "child tag defining a drawable");
+ }
+ dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
+ }
+
+ return mState.addTransition(fromId, toId, dr, reversible);
+ }
+
+ private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ // This allows state list drawable item elements to be themed at
+ // inflation time but does NOT make them work for Zygote preload.
+ final TypedArray a = obtainAttributes(r, theme, attrs,
+ R.styleable.AnimatedStateListDrawableItem);
+ final int keyframeId = a.getResourceId(R.styleable.AnimatedStateListDrawableItem_id, 0);
+ Drawable dr = a.getDrawable(R.styleable.AnimatedStateListDrawableItem_drawable);
+ a.recycle();
+
+ final int[] states = extractStateSet(attrs);
+
+ // Loading child elements modifies the state of the AttributeSet's
+ // underlying parser, so it needs to happen after obtaining
+ // attributes and extracting states.
+ if (dr == null) {
+ int type;
+ while ((type = parser.next()) == XmlPullParser.TEXT) {
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException(
+ parser.getPositionDescription()
+ + ": <item> tag requires a 'drawable' attribute or "
+ + "child tag defining a drawable");
+ }
+ dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
+ }
+
+ return mState.addStateSet(states, dr, keyframeId);
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mState.mutate();
+ mMutated = true;
+ }
+
+ return this;
+ }
+
+ @Override
+ AnimatedStateListState cloneConstantState() {
+ return new AnimatedStateListState(mState, this, null);
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mMutated = false;
+ }
+
+ static class AnimatedStateListState extends StateListState {
+ // REVERSED_BIT is indicating the current transition's direction.
+ private static final long REVERSED_BIT = 0x100000000l;
+
+ // REVERSIBLE_FLAG_BIT is indicating whether the whole transition has
+ // reversible flag set to true.
+ private static final long REVERSIBLE_FLAG_BIT = 0x200000000l;
+
+ int[] mAnimThemeAttrs;
+
+ LongSparseLongArray mTransitions;
+ SparseIntArray mStateIds;
+
+ AnimatedStateListState(@Nullable AnimatedStateListState orig,
+ @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) {
+ super(orig, owner, res);
+
+ if (orig != null) {
+ // Perform a shallow copy and rely on mutate() to deep-copy.
+ mAnimThemeAttrs = orig.mAnimThemeAttrs;
+ mTransitions = orig.mTransitions;
+ mStateIds = orig.mStateIds;
+ } else {
+ mTransitions = new LongSparseLongArray();
+ mStateIds = new SparseIntArray();
+ }
+ }
+
+ void mutate() {
+ mTransitions = mTransitions.clone();
+ mStateIds = mStateIds.clone();
+ }
+
+ int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) {
+ final int pos = super.addChild(anim);
+ final long keyFromTo = generateTransitionKey(fromId, toId);
+ long reversibleBit = 0;
+ if (reversible) {
+ reversibleBit = REVERSIBLE_FLAG_BIT;
+ }
+ mTransitions.append(keyFromTo, pos | reversibleBit);
+
+ if (reversible) {
+ final long keyToFrom = generateTransitionKey(toId, fromId);
+ mTransitions.append(keyToFrom, pos | REVERSED_BIT | reversibleBit);
+ }
+
+ return pos;
+ }
+
+ int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) {
+ final int index = super.addStateSet(stateSet, drawable);
+ mStateIds.put(index, id);
+ return index;
+ }
+
+ int indexOfKeyframe(@NonNull int[] stateSet) {
+ final int index = super.indexOfStateSet(stateSet);
+ if (index >= 0) {
+ return index;
+ }
+
+ return super.indexOfStateSet(StateSet.WILD_CARD);
+ }
+
+ int getKeyframeIdAt(int index) {
+ return index < 0 ? 0 : mStateIds.get(index, 0);
+ }
+
+ int indexOfTransition(int fromId, int toId) {
+ final long keyFromTo = generateTransitionKey(fromId, toId);
+ return (int) mTransitions.get(keyFromTo, -1);
+ }
+
+ boolean isTransitionReversed(int fromId, int toId) {
+ final long keyFromTo = generateTransitionKey(fromId, toId);
+ return (mTransitions.get(keyFromTo, -1) & REVERSED_BIT) != 0;
+ }
+
+ boolean transitionHasReversibleFlag(int fromId, int toId) {
+ final long keyFromTo = generateTransitionKey(fromId, toId);
+ return (mTransitions.get(keyFromTo, -1) & REVERSIBLE_FLAG_BIT) != 0;
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mAnimThemeAttrs != null || super.canApplyTheme();
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new AnimatedStateListDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new AnimatedStateListDrawable(this, res);
+ }
+
+ private static long generateTransitionKey(int fromId, int toId) {
+ return (long) fromId << 32 | toId;
+ }
+ }
+
+ @Override
+ protected void setConstantState(@NonNull DrawableContainerState state) {
+ super.setConstantState(state);
+
+ if (state instanceof AnimatedStateListState) {
+ mState = (AnimatedStateListState) state;
+ }
+ }
+
+ private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) {
+ super(null);
+
+ // Every animated state list drawable has its own constant state.
+ final AnimatedStateListState newState = new AnimatedStateListState(state, this, res);
+ setConstantState(newState);
+ onStateChange(getState());
+ jumpToCurrentState();
+ }
+
+ /**
+ * Interpolates between frames with respect to their individual durations.
+ */
+ private static class FrameInterpolator implements TimeInterpolator {
+ private int[] mFrameTimes;
+ private int mFrames;
+ private int mTotalDuration;
+
+ public FrameInterpolator(AnimationDrawable d, boolean reversed) {
+ updateFrames(d, reversed);
+ }
+
+ public int updateFrames(AnimationDrawable d, boolean reversed) {
+ final int N = d.getNumberOfFrames();
+ mFrames = N;
+
+ if (mFrameTimes == null || mFrameTimes.length < N) {
+ mFrameTimes = new int[N];
+ }
+
+ final int[] frameTimes = mFrameTimes;
+ int totalDuration = 0;
+ for (int i = 0; i < N; i++) {
+ final int duration = d.getDuration(reversed ? N - i - 1 : i);
+ frameTimes[i] = duration;
+ totalDuration += duration;
+ }
+
+ mTotalDuration = totalDuration;
+ return totalDuration;
+ }
+
+ public int getTotalDuration() {
+ return mTotalDuration;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ final int elapsed = (int) (input * mTotalDuration + 0.5f);
+ final int N = mFrames;
+ final int[] frameTimes = mFrameTimes;
+
+ // Find the current frame and remaining time within that frame.
+ int remaining = elapsed;
+ int i = 0;
+ while (i < N && remaining >= frameTimes[i]) {
+ remaining -= frameTimes[i];
+ i++;
+ }
+
+ // Remaining time is relative of total duration.
+ final float frameElapsed;
+ if (i < N) {
+ frameElapsed = remaining / (float) mTotalDuration;
+ } else {
+ frameElapsed = 0;
+ }
+
+ return i / (float) N + frameElapsed;
+ }
+ }
+}
diff --git a/android/graphics/drawable/AnimatedVectorDrawable.java b/android/graphics/drawable/AnimatedVectorDrawable.java
new file mode 100644
index 0000000..90d6ab8
--- /dev/null
+++ b/android/graphics/drawable/AnimatedVectorDrawable.java
@@ -0,0 +1,1826 @@
+/*
+ * Copyright (C) 2014 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 android.graphics.drawable;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.app.Application;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Insets;
+import android.graphics.Outline;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.LongArray;
+import android.util.PathParser;
+import android.util.Property;
+import android.util.TimeUtils;
+import android.view.Choreographer;
+import android.view.DisplayListCanvas;
+import android.view.RenderNode;
+import android.view.RenderNodeAnimatorSetHelper;
+import android.view.View;
+
+import com.android.internal.R;
+import com.android.internal.util.VirtualRefBasePtr;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+import dalvik.annotation.optimization.FastNative;
+
+/**
+ * This class animates properties of a {@link android.graphics.drawable.VectorDrawable} with
+ * animations defined using {@link android.animation.ObjectAnimator} or
+ * {@link android.animation.AnimatorSet}.
+ * <p>
+ * Starting from API 25, AnimatedVectorDrawable runs on RenderThread (as opposed to on UI thread for
+ * earlier APIs). This means animations in AnimatedVectorDrawable can remain smooth even when there
+ * is heavy workload on the UI thread. Note: If the UI thread is unresponsive, RenderThread may
+ * continue animating until the UI thread is capable of pushing another frame. Therefore, it is not
+ * possible to precisely coordinate a RenderThread-enabled AnimatedVectorDrawable with UI thread
+ * animations. Additionally,
+ * {@link android.graphics.drawable.Animatable2.AnimationCallback#onAnimationEnd(Drawable)} will be
+ * called the frame after the AnimatedVectorDrawable finishes on the RenderThread.
+ * </p>
+ * <p>
+ * AnimatedVectorDrawable can be defined in either <a href="#ThreeXML">three separate XML files</a>,
+ * or <a href="#OneXML">one XML</a>.
+ * </p>
+ * <a name="ThreeXML"></a>
+ * <h3>Define an AnimatedVectorDrawable in three separate XML files</h3>
+ * <ul>
+ * <a name="VDExample"></a>
+ * <li><h4>XML for the VectorDrawable containing properties to be animated</h4>
+ * <p>
+ * Animations can be performed on the animatable attributes in
+ * {@link android.graphics.drawable.VectorDrawable}. These attributes will be animated by
+ * {@link android.animation.ObjectAnimator}. The ObjectAnimator's target can be the root element,
+ * a group element or a path element. The targeted elements need to be named uniquely within
+ * the same VectorDrawable. Elements without animation do not need to be named.
+ * </p>
+ * <p>
+ * Here are all the animatable attributes in {@link android.graphics.drawable.VectorDrawable}:
+ * <table border="2" align="center" cellpadding="5">
+ * <thead>
+ * <tr>
+ * <th>Element Name</th>
+ * <th>Animatable attribute name</th>
+ * </tr>
+ * </thead>
+ * <tr>
+ * <td><vector></td>
+ * <td>alpha</td>
+ * </tr>
+ * <tr>
+ * <td rowspan="7"><group></td>
+ * <td>rotation</td>
+ * </tr>
+ * <tr>
+ * <td>pivotX</td>
+ * </tr>
+ * <tr>
+ * <td>pivotY</td>
+ * </tr>
+ * <tr>
+ * <td>scaleX</td>
+ * </tr>
+ * <tr>
+ * <td>scaleY</td>
+ * </tr>
+ * <tr>
+ * <td>translateX</td>
+ * </tr>
+ * <tr>
+ * <td>translateY</td>
+ * </tr>
+ * <tr>
+ * <td rowspan="8"><path></td>
+ * <td>pathData</td>
+ * </tr>
+ * <tr>
+ * <td>fillColor</td>
+ * </tr>
+ * <tr>
+ * <td>strokeColor</td>
+ * </tr>
+ * <tr>
+ * <td>strokeWidth</td>
+ * </tr>
+ * <tr>
+ * <td>strokeAlpha</td>
+ * </tr>
+ * <tr>
+ * <td>fillAlpha</td>
+ * </tr>
+ * <tr>
+ * <td>trimPathStart</td>
+ * </tr>
+ * <tr>
+ * <td>trimPathOffset</td>
+ * </tr>
+ * <tr>
+ * <td><clip-path></td>
+ * <td>pathData</td>
+ * </tr>
+ * </table>
+ * </p>
+ * Below is an example of a VectorDrawable defined in vectordrawable.xml. This VectorDrawable is
+ * referred to by its file name (not including file suffix) in the
+ * <a href="AVDExample">AnimatedVectorDrawable XML example</a>.
+ * <pre>
+ * <vector xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:height="64dp"
+ * android:width="64dp"
+ * android:viewportHeight="600"
+ * android:viewportWidth="600" >
+ * <group
+ * android:name="rotationGroup"
+ * android:pivotX="300.0"
+ * android:pivotY="300.0"
+ * android:rotation="45.0" >
+ * <path
+ * android:name="v"
+ * android:fillColor="#000000"
+ * android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
+ * </group>
+ * </vector>
+ * </pre></li>
+ *
+ * <a name="AVDExample"></a>
+ * <li><h4>XML for AnimatedVectorDrawable</h4>
+ * <p>
+ * An AnimatedVectorDrawable element has a VectorDrawable attribute, and one or more target
+ * element(s). The target element can specify its target by android:name attribute, and link the
+ * target with the proper ObjectAnimator or AnimatorSet by android:animation attribute.
+ * </p>
+ * The following code sample defines an AnimatedVectorDrawable. Note that the names refer to the
+ * groups and paths in the <a href="#VDExample">VectorDrawable XML above</a>.
+ * <pre>
+ * <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:drawable="@drawable/vectordrawable" >
+ * <target
+ * android:name="rotationGroup"
+ * android:animation="@anim/rotation" />
+ * <target
+ * android:name="v"
+ * android:animation="@anim/path_morph" />
+ * </animated-vector>
+ * </pre>
+ * </li>
+ *
+ * <li><h4>XML for Animations defined using ObjectAnimator or AnimatorSet</h4>
+ * <p>
+ * From the previous <a href="#AVDExample">example of AnimatedVectorDrawable</a>, two animations
+ * were used: rotation.xml and path_morph.xml.
+ * </p>
+ * rotation.xml rotates the target group from 0 degree to 360 degrees over 6000ms:
+ * <pre>
+ * <objectAnimator
+ * android:duration="6000"
+ * android:propertyName="rotation"
+ * android:valueFrom="0"
+ * android:valueTo="360" />
+ * </pre>
+ *
+ * path_morph.xml morphs the path from one shape into the other. Note that the paths must be
+ * compatible for morphing. Specifically, the paths must have the same commands, in the same order,
+ * and must have the same number of parameters for each command. It is recommended to store path
+ * strings as string resources for reuse.
+ * <pre>
+ * <set xmlns:android="http://schemas.android.com/apk/res/android">
+ * <objectAnimator
+ * android:duration="3000"
+ * android:propertyName="pathData"
+ * android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z"
+ * android:valueTo="M300,70 l 0,-70 70,0 0,140 -70,0 z"
+ * android:valueType="pathType"/>
+ * </set>
+ * </pre>
+ * </ul>
+ * <a name="OneXML"></a>
+ * <h3>Define an AnimatedVectorDrawable all in one XML file</h3>
+ * <p>
+ * Since the AAPT tool supports a new format that bundles several related XML files together, we can
+ * merge the XML files from the previous examples into one XML file:
+ * </p>
+ * <pre>
+ * <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ * xmlns:aapt="http://schemas.android.com/aapt" >
+ * <aapt:attr name="android:drawable">
+ * <vector
+ * android:height="64dp"
+ * android:width="64dp"
+ * android:viewportHeight="600"
+ * android:viewportWidth="600" >
+ * <group
+ * android:name="rotationGroup"
+ * android:pivotX="300.0"
+ * android:pivotY="300.0"
+ * android:rotation="45.0" >
+ * <path
+ * android:name="v"
+ * android:fillColor="#000000"
+ * android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
+ * </group>
+ * </vector>
+ * </aapt:attr>
+ *
+ * <target android:name="rotationGroup"> *
+ * <aapt:attr name="android:animation">
+ * <objectAnimator
+ * android:duration="6000"
+ * android:propertyName="rotation"
+ * android:valueFrom="0"
+ * android:valueTo="360" />
+ * </aapt:attr>
+ * </target>
+ *
+ * <target android:name="v" >
+ * <aapt:attr name="android:animation">
+ * <set>
+ * <objectAnimator
+ * android:duration="3000"
+ * android:propertyName="pathData"
+ * android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z"
+ * android:valueTo="M300,70 l 0,-70 70,0 0,140 -70,0 z"
+ * android:valueType="pathType"/>
+ * </set>
+ * </aapt:attr>
+ * </target>
+ * </animated-vector>
+ * </pre>
+ *
+ * @attr ref android.R.styleable#AnimatedVectorDrawable_drawable
+ * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_name
+ * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_animation
+ */
+public class AnimatedVectorDrawable extends Drawable implements Animatable2 {
+ private static final String LOGTAG = "AnimatedVectorDrawable";
+
+ private static final String ANIMATED_VECTOR = "animated-vector";
+ private static final String TARGET = "target";
+
+ private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;
+
+ /** Local, mutable animator set. */
+ private VectorDrawableAnimator mAnimatorSet;
+
+ /**
+ * The resources against which this drawable was created. Used to attempt
+ * to inflate animators if applyTheme() doesn't get called.
+ */
+ private Resources mRes;
+
+ private AnimatedVectorDrawableState mAnimatedVectorState;
+
+ /** The animator set that is parsed from the xml. */
+ private AnimatorSet mAnimatorSetFromXml = null;
+
+ private boolean mMutated;
+
+ /** Use a internal AnimatorListener to support callbacks during animation events. */
+ private ArrayList<Animatable2.AnimationCallback> mAnimationCallbacks = null;
+ private AnimatorListener mAnimatorListener = null;
+
+ public AnimatedVectorDrawable() {
+ this(null, null);
+ }
+
+ private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) {
+ mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res);
+ mAnimatorSet = new VectorDrawableAnimatorRT(this);
+ mRes = res;
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mAnimatedVectorState = new AnimatedVectorDrawableState(
+ mAnimatedVectorState, mCallback, mRes);
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ if (mAnimatedVectorState.mVectorDrawable != null) {
+ mAnimatedVectorState.mVectorDrawable.clearMutated();
+ }
+ mMutated = false;
+ }
+
+ /**
+ * In order to avoid breaking old apps, we only throw exception on invalid VectorDrawable
+ * animations for apps targeting N and later. For older apps, we ignore (i.e. quietly skip)
+ * these animations.
+ *
+ * @return whether invalid animations for vector drawable should be ignored.
+ */
+ private static boolean shouldIgnoreInvalidAnimation() {
+ Application app = ActivityThread.currentApplication();
+ if (app == null || app.getApplicationInfo() == null) {
+ return true;
+ }
+ if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations();
+ return mAnimatedVectorState;
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations();
+ }
+
+ /**
+ * Draws the AnimatedVectorDrawable into the given canvas.
+ * <p>
+ * <strong>Note:</strong> Calling this method with a software canvas when the
+ * AnimatedVectorDrawable is being animated on RenderThread (for API 25 and later) may yield
+ * outdated result, as the UI thread is not guaranteed to be in sync with RenderThread on
+ * VectorDrawable's property changes during RenderThread animations.
+ * </p>
+ *
+ * @param canvas The canvas to draw into
+ */
+ @Override
+ public void draw(Canvas canvas) {
+ if (!canvas.isHardwareAccelerated() && mAnimatorSet instanceof VectorDrawableAnimatorRT) {
+ // If we have SW canvas and the RT animation is waiting to start, We need to fallback
+ // to UI thread animation for AVD.
+ if (!mAnimatorSet.isRunning() &&
+ ((VectorDrawableAnimatorRT) mAnimatorSet).mPendingAnimationActions.size() > 0) {
+ fallbackOntoUI();
+ }
+ }
+ mAnimatorSet.onDraw(canvas);
+ mAnimatedVectorState.mVectorDrawable.draw(canvas);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
+ }
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ return mAnimatedVectorState.mVectorDrawable.setState(state);
+ }
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ return mAnimatedVectorState.mVectorDrawable.setLevel(level);
+ }
+
+ @Override
+ public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
+ return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection);
+ }
+
+ /**
+ * For API 25 and later, AnimatedVectorDrawable runs on RenderThread. Therefore, when the
+ * root alpha is being animated, this getter does not guarantee to return an up-to-date alpha
+ * value.
+ *
+ * @return the containing vector drawable's root alpha value.
+ */
+ @Override
+ public int getAlpha() {
+ return mAnimatedVectorState.mVectorDrawable.getAlpha();
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
+ }
+
+ @Override
+ public ColorFilter getColorFilter() {
+ return mAnimatedVectorState.mVectorDrawable.getColorFilter();
+ }
+
+ @Override
+ public void setTintList(ColorStateList tint) {
+ mAnimatedVectorState.mVectorDrawable.setTintList(tint);
+ }
+
+ @Override
+ public void setHotspot(float x, float y) {
+ mAnimatedVectorState.mVectorDrawable.setHotspot(x, y);
+ }
+
+ @Override
+ public void setHotspotBounds(int left, int top, int right, int bottom) {
+ mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom);
+ }
+
+ @Override
+ public void setTintMode(PorterDuff.Mode tintMode) {
+ mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ if (mAnimatorSet.isInfinite() && mAnimatorSet.isStarted()) {
+ if (visible) {
+ // Resume the infinite animation when the drawable becomes visible again.
+ mAnimatorSet.resume();
+ } else {
+ // Pause the infinite animation once the drawable is no longer visible.
+ mAnimatorSet.pause();
+ }
+ }
+ mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
+ return super.setVisible(visible, restart);
+ }
+
+ @Override
+ public boolean isStateful() {
+ return mAnimatedVectorState.mVectorDrawable.isStateful();
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ mAnimatedVectorState.mVectorDrawable.getOutline(outline);
+ }
+
+ /** @hide */
+ @Override
+ public Insets getOpticalInsets() {
+ return mAnimatedVectorState.mVectorDrawable.getOpticalInsets();
+ }
+
+ @Override
+ public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
+ throws XmlPullParserException, IOException {
+ final AnimatedVectorDrawableState state = mAnimatedVectorState;
+
+ int eventType = parser.getEventType();
+ float pathErrorScale = 1;
+ final int innerDepth = parser.getDepth() + 1;
+
+ // Parse everything until the end of the animated-vector element.
+ while (eventType != XmlPullParser.END_DOCUMENT
+ && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
+ if (eventType == XmlPullParser.START_TAG) {
+ final String tagName = parser.getName();
+ if (ANIMATED_VECTOR.equals(tagName)) {
+ final TypedArray a = obtainAttributes(res, theme, attrs,
+ R.styleable.AnimatedVectorDrawable);
+ int drawableRes = a.getResourceId(
+ R.styleable.AnimatedVectorDrawable_drawable, 0);
+ if (drawableRes != 0) {
+ VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable(
+ drawableRes, theme).mutate();
+ vectorDrawable.setAllowCaching(false);
+ vectorDrawable.setCallback(mCallback);
+ pathErrorScale = vectorDrawable.getPixelSize();
+ if (state.mVectorDrawable != null) {
+ state.mVectorDrawable.setCallback(null);
+ }
+ state.mVectorDrawable = vectorDrawable;
+ }
+ a.recycle();
+ } else if (TARGET.equals(tagName)) {
+ final TypedArray a = obtainAttributes(res, theme, attrs,
+ R.styleable.AnimatedVectorDrawableTarget);
+ final String target = a.getString(
+ R.styleable.AnimatedVectorDrawableTarget_name);
+ final int animResId = a.getResourceId(
+ R.styleable.AnimatedVectorDrawableTarget_animation, 0);
+ if (animResId != 0) {
+ if (theme != null) {
+ // The animator here could be ObjectAnimator or AnimatorSet.
+ final Animator animator = AnimatorInflater.loadAnimator(
+ res, theme, animResId, pathErrorScale);
+ updateAnimatorProperty(animator, target, state.mVectorDrawable,
+ state.mShouldIgnoreInvalidAnim);
+ state.addTargetAnimator(target, animator);
+ } else {
+ // The animation may be theme-dependent. As a
+ // workaround until Animator has full support for
+ // applyTheme(), postpone loading the animator
+ // until we have a theme in applyTheme().
+ state.addPendingAnimator(animResId, pathErrorScale, target);
+
+ }
+ }
+ a.recycle();
+ }
+ }
+
+ eventType = parser.next();
+ }
+
+ // If we don't have any pending animations, we don't need to hold a
+ // reference to the resources.
+ mRes = state.mPendingAnims == null ? null : res;
+ }
+
+ private static void updateAnimatorProperty(Animator animator, String targetName,
+ VectorDrawable vectorDrawable, boolean ignoreInvalidAnim) {
+ if (animator instanceof ObjectAnimator) {
+ // Change the property of the Animator from using reflection based on the property
+ // name to a Property object that wraps the setter and getter for modifying that
+ // specific property for a given object. By replacing the reflection with a direct call,
+ // we can largely reduce the time it takes for a animator to modify a VD property.
+ PropertyValuesHolder[] holders = ((ObjectAnimator) animator).getValues();
+ for (int i = 0; i < holders.length; i++) {
+ PropertyValuesHolder pvh = holders[i];
+ String propertyName = pvh.getPropertyName();
+ Object targetNameObj = vectorDrawable.getTargetByName(targetName);
+ Property property = null;
+ if (targetNameObj instanceof VectorDrawable.VObject) {
+ property = ((VectorDrawable.VObject) targetNameObj).getProperty(propertyName);
+ } else if (targetNameObj instanceof VectorDrawable.VectorDrawableState) {
+ property = ((VectorDrawable.VectorDrawableState) targetNameObj)
+ .getProperty(propertyName);
+ }
+ if (property != null) {
+ if (containsSameValueType(pvh, property)) {
+ pvh.setProperty(property);
+ } else if (!ignoreInvalidAnim) {
+ throw new RuntimeException("Wrong valueType for Property: " + propertyName
+ + ". Expected type: " + property.getType().toString() + ". Actual "
+ + "type defined in resources: " + pvh.getValueType().toString());
+
+ }
+ }
+ }
+ } else if (animator instanceof AnimatorSet) {
+ for (Animator anim : ((AnimatorSet) animator).getChildAnimations()) {
+ updateAnimatorProperty(anim, targetName, vectorDrawable, ignoreInvalidAnim);
+ }
+ }
+ }
+
+ private static boolean containsSameValueType(PropertyValuesHolder holder, Property property) {
+ Class type1 = holder.getValueType();
+ Class type2 = property.getType();
+ if (type1 == float.class || type1 == Float.class) {
+ return type2 == float.class || type2 == Float.class;
+ } else if (type1 == int.class || type1 == Integer.class) {
+ return type2 == int.class || type2 == Integer.class;
+ } else {
+ return type1 == type2;
+ }
+ }
+
+ /**
+ * Force to animate on UI thread.
+ * @hide
+ */
+ public void forceAnimationOnUI() {
+ if (mAnimatorSet instanceof VectorDrawableAnimatorRT) {
+ VectorDrawableAnimatorRT animator = (VectorDrawableAnimatorRT) mAnimatorSet;
+ if (animator.isRunning()) {
+ throw new UnsupportedOperationException("Cannot force Animated Vector Drawable to" +
+ " run on UI thread when the animation has started on RenderThread.");
+ }
+ fallbackOntoUI();
+ }
+ }
+
+ private void fallbackOntoUI() {
+ if (mAnimatorSet instanceof VectorDrawableAnimatorRT) {
+ VectorDrawableAnimatorRT oldAnim = (VectorDrawableAnimatorRT) mAnimatorSet;
+ mAnimatorSet = new VectorDrawableAnimatorUI(this);
+ if (mAnimatorSetFromXml != null) {
+ mAnimatorSet.init(mAnimatorSetFromXml);
+ }
+ // Transfer the listener from RT animator to UI animator
+ if (oldAnim.mListener != null) {
+ mAnimatorSet.setListener(oldAnim.mListener);
+ }
+ oldAnim.transferPendingActions(mAnimatorSet);
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return (mAnimatedVectorState != null && mAnimatedVectorState.canApplyTheme())
+ || super.canApplyTheme();
+ }
+
+ @Override
+ public void applyTheme(Theme t) {
+ super.applyTheme(t);
+
+ final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable;
+ if (vectorDrawable != null && vectorDrawable.canApplyTheme()) {
+ vectorDrawable.applyTheme(t);
+ }
+
+ if (t != null) {
+ mAnimatedVectorState.inflatePendingAnimators(t.getResources(), t);
+ }
+
+ // If we don't have any pending animations, we don't need to hold a
+ // reference to the resources.
+ if (mAnimatedVectorState.mPendingAnims == null) {
+ mRes = null;
+ }
+ }
+
+ private static class AnimatedVectorDrawableState extends ConstantState {
+ @Config int mChangingConfigurations;
+ VectorDrawable mVectorDrawable;
+
+ private final boolean mShouldIgnoreInvalidAnim;
+
+ /** Animators that require a theme before inflation. */
+ ArrayList<PendingAnimator> mPendingAnims;
+
+ /** Fully inflated animators awaiting cloning into an AnimatorSet. */
+ ArrayList<Animator> mAnimators;
+
+ /** Map of animators to their target object names */
+ ArrayMap<Animator, String> mTargetNameMap;
+
+ public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy,
+ Callback owner, Resources res) {
+ mShouldIgnoreInvalidAnim = shouldIgnoreInvalidAnimation();
+ if (copy != null) {
+ mChangingConfigurations = copy.mChangingConfigurations;
+
+ if (copy.mVectorDrawable != null) {
+ final ConstantState cs = copy.mVectorDrawable.getConstantState();
+ if (res != null) {
+ mVectorDrawable = (VectorDrawable) cs.newDrawable(res);
+ } else {
+ mVectorDrawable = (VectorDrawable) cs.newDrawable();
+ }
+ mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate();
+ mVectorDrawable.setCallback(owner);
+ mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection());
+ mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds());
+ mVectorDrawable.setAllowCaching(false);
+ }
+
+ if (copy.mAnimators != null) {
+ mAnimators = new ArrayList<>(copy.mAnimators);
+ }
+
+ if (copy.mTargetNameMap != null) {
+ mTargetNameMap = new ArrayMap<>(copy.mTargetNameMap);
+ }
+
+ if (copy.mPendingAnims != null) {
+ mPendingAnims = new ArrayList<>(copy.mPendingAnims);
+ }
+ } else {
+ mVectorDrawable = new VectorDrawable();
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return (mVectorDrawable != null && mVectorDrawable.canApplyTheme())
+ || mPendingAnims != null || super.canApplyTheme();
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new AnimatedVectorDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new AnimatedVectorDrawable(this, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations;
+ }
+
+ public void addPendingAnimator(int resId, float pathErrorScale, String target) {
+ if (mPendingAnims == null) {
+ mPendingAnims = new ArrayList<>(1);
+ }
+ mPendingAnims.add(new PendingAnimator(resId, pathErrorScale, target));
+ }
+
+ public void addTargetAnimator(String targetName, Animator animator) {
+ if (mAnimators == null) {
+ mAnimators = new ArrayList<>(1);
+ mTargetNameMap = new ArrayMap<>(1);
+ }
+ mAnimators.add(animator);
+ mTargetNameMap.put(animator, targetName);
+
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.v(LOGTAG, "add animator for target " + targetName + " " + animator);
+ }
+ }
+
+ /**
+ * Prepares a local set of mutable animators based on the constant
+ * state.
+ * <p>
+ * If there are any pending uninflated animators, attempts to inflate
+ * them immediately against the provided resources object.
+ *
+ * @param animatorSet the animator set to which the animators should
+ * be added
+ * @param res the resources against which to inflate any pending
+ * animators, or {@code null} if not available
+ */
+ public void prepareLocalAnimators(@NonNull AnimatorSet animatorSet,
+ @Nullable Resources res) {
+ // Check for uninflated animators. We can remove this after we add
+ // support for Animator.applyTheme(). See comments in inflate().
+ if (mPendingAnims != null) {
+ // Attempt to load animators without applying a theme.
+ if (res != null) {
+ inflatePendingAnimators(res, null);
+ } else {
+ Log.e(LOGTAG, "Failed to load animators. Either the AnimatedVectorDrawable"
+ + " must be created using a Resources object or applyTheme() must be"
+ + " called with a non-null Theme object.");
+ }
+
+ mPendingAnims = null;
+ }
+
+ // Perform a deep copy of the constant state's animators.
+ final int count = mAnimators == null ? 0 : mAnimators.size();
+ if (count > 0) {
+ final Animator firstAnim = prepareLocalAnimator(0);
+ final AnimatorSet.Builder builder = animatorSet.play(firstAnim);
+ for (int i = 1; i < count; ++i) {
+ final Animator nextAnim = prepareLocalAnimator(i);
+ builder.with(nextAnim);
+ }
+ }
+ }
+
+ /**
+ * Prepares a local animator for the given index within the constant
+ * state's list of animators.
+ *
+ * @param index the index of the animator within the constant state
+ */
+ private Animator prepareLocalAnimator(int index) {
+ final Animator animator = mAnimators.get(index);
+ final Animator localAnimator = animator.clone();
+ final String targetName = mTargetNameMap.get(animator);
+ final Object target = mVectorDrawable.getTargetByName(targetName);
+ if (!mShouldIgnoreInvalidAnim) {
+ if (target == null) {
+ throw new IllegalStateException("Target with the name \"" + targetName
+ + "\" cannot be found in the VectorDrawable to be animated.");
+ } else if (!(target instanceof VectorDrawable.VectorDrawableState)
+ && !(target instanceof VectorDrawable.VObject)) {
+ throw new UnsupportedOperationException("Target should be either VGroup, VPath,"
+ + " or ConstantState, " + target.getClass() + " is not supported");
+ }
+ }
+ localAnimator.setTarget(target);
+ return localAnimator;
+ }
+
+ /**
+ * Inflates pending animators, if any, against a theme. Clears the list of
+ * pending animators.
+ *
+ * @param t the theme against which to inflate the animators
+ */
+ public void inflatePendingAnimators(@NonNull Resources res, @Nullable Theme t) {
+ final ArrayList<PendingAnimator> pendingAnims = mPendingAnims;
+ if (pendingAnims != null) {
+ mPendingAnims = null;
+
+ for (int i = 0, count = pendingAnims.size(); i < count; i++) {
+ final PendingAnimator pendingAnimator = pendingAnims.get(i);
+ final Animator animator = pendingAnimator.newInstance(res, t);
+ updateAnimatorProperty(animator, pendingAnimator.target, mVectorDrawable,
+ mShouldIgnoreInvalidAnim);
+ addTargetAnimator(pendingAnimator.target, animator);
+ }
+ }
+ }
+
+ /**
+ * Basically a constant state for Animators until we actually implement
+ * constant states for Animators.
+ */
+ private static class PendingAnimator {
+ public final int animResId;
+ public final float pathErrorScale;
+ public final String target;
+
+ public PendingAnimator(int animResId, float pathErrorScale, String target) {
+ this.animResId = animResId;
+ this.pathErrorScale = pathErrorScale;
+ this.target = target;
+ }
+
+ public Animator newInstance(Resources res, Theme theme) {
+ return AnimatorInflater.loadAnimator(res, theme, animResId, pathErrorScale);
+ }
+ }
+ }
+
+ @Override
+ public boolean isRunning() {
+ return mAnimatorSet.isRunning();
+ }
+
+ /**
+ * Resets the AnimatedVectorDrawable to the start state as specified in the animators.
+ */
+ public void reset() {
+ ensureAnimatorSet();
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.w(LOGTAG, "calling reset on AVD: " +
+ ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState)
+ getConstantState()).mVectorDrawable.getConstantState()).mRootName
+ + ", at: " + this);
+ }
+ mAnimatorSet.reset();
+ }
+
+ @Override
+ public void start() {
+ ensureAnimatorSet();
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.w(LOGTAG, "calling start on AVD: " +
+ ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState)
+ getConstantState()).mVectorDrawable.getConstantState()).mRootName
+ + ", at: " + this);
+ }
+ mAnimatorSet.start();
+ }
+
+ @NonNull
+ private void ensureAnimatorSet() {
+ if (mAnimatorSetFromXml == null) {
+ // TODO: Skip the AnimatorSet creation and init the VectorDrawableAnimator directly
+ // with a list of LocalAnimators.
+ mAnimatorSetFromXml = new AnimatorSet();
+ mAnimatedVectorState.prepareLocalAnimators(mAnimatorSetFromXml, mRes);
+ mAnimatorSet.init(mAnimatorSetFromXml);
+ mRes = null;
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.w(LOGTAG, "calling stop on AVD: " +
+ ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState)
+ getConstantState()).mVectorDrawable.getConstantState())
+ .mRootName + ", at: " + this);
+ }
+ mAnimatorSet.end();
+ }
+
+ /**
+ * Reverses ongoing animations or starts pending animations in reverse.
+ * <p>
+ * NOTE: Only works if all animations support reverse. Otherwise, this will
+ * do nothing.
+ * @hide
+ */
+ public void reverse() {
+ ensureAnimatorSet();
+
+ // Only reverse when all the animators can be reversed.
+ if (!canReverse()) {
+ Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()");
+ return;
+ }
+
+ mAnimatorSet.reverse();
+ }
+
+ /**
+ * @hide
+ */
+ public boolean canReverse() {
+ return mAnimatorSet.canReverse();
+ }
+
+ private final Callback mCallback = new Callback() {
+ @Override
+ public void invalidateDrawable(@NonNull Drawable who) {
+ invalidateSelf();
+ }
+
+ @Override
+ public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+ scheduleSelf(what, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+ unscheduleSelf(what);
+ }
+ };
+
+ @Override
+ public void registerAnimationCallback(@NonNull AnimationCallback callback) {
+ if (callback == null) {
+ return;
+ }
+
+ // Add listener accordingly.
+ if (mAnimationCallbacks == null) {
+ mAnimationCallbacks = new ArrayList<>();
+ }
+
+ mAnimationCallbacks.add(callback);
+
+ if (mAnimatorListener == null) {
+ // Create a animator listener and trigger the callback events when listener is
+ // triggered.
+ mAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ ArrayList<AnimationCallback> tmpCallbacks = new ArrayList<>(mAnimationCallbacks);
+ int size = tmpCallbacks.size();
+ for (int i = 0; i < size; i ++) {
+ tmpCallbacks.get(i).onAnimationStart(AnimatedVectorDrawable.this);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ArrayList<AnimationCallback> tmpCallbacks = new ArrayList<>(mAnimationCallbacks);
+ int size = tmpCallbacks.size();
+ for (int i = 0; i < size; i ++) {
+ tmpCallbacks.get(i).onAnimationEnd(AnimatedVectorDrawable.this);
+ }
+ }
+ };
+ }
+ mAnimatorSet.setListener(mAnimatorListener);
+ }
+
+ // A helper function to clean up the animator listener in the mAnimatorSet.
+ private void removeAnimatorSetListener() {
+ if (mAnimatorListener != null) {
+ mAnimatorSet.removeListener(mAnimatorListener);
+ mAnimatorListener = null;
+ }
+ }
+
+ @Override
+ public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) {
+ if (mAnimationCallbacks == null || callback == null) {
+ // Nothing to be removed.
+ return false;
+ }
+ boolean removed = mAnimationCallbacks.remove(callback);
+
+ // When the last call back unregistered, remove the listener accordingly.
+ if (mAnimationCallbacks.size() == 0) {
+ removeAnimatorSetListener();
+ }
+ return removed;
+ }
+
+ @Override
+ public void clearAnimationCallbacks() {
+ removeAnimatorSetListener();
+ if (mAnimationCallbacks == null) {
+ return;
+ }
+
+ mAnimationCallbacks.clear();
+ }
+
+ private interface VectorDrawableAnimator {
+ void init(@NonNull AnimatorSet set);
+ void start();
+ void end();
+ void reset();
+ void reverse();
+ boolean canReverse();
+ void setListener(AnimatorListener listener);
+ void removeListener(AnimatorListener listener);
+ void onDraw(Canvas canvas);
+ boolean isStarted();
+ boolean isRunning();
+ boolean isInfinite();
+ void pause();
+ void resume();
+ }
+
+ private static class VectorDrawableAnimatorUI implements VectorDrawableAnimator {
+ // mSet is only initialized in init(). So we need to check whether it is null before any
+ // operation.
+ private AnimatorSet mSet = null;
+ private final Drawable mDrawable;
+ // Caching the listener in the case when listener operation is called before the mSet is
+ // setup by init().
+ private ArrayList<AnimatorListener> mListenerArray = null;
+ private boolean mIsInfinite = false;
+
+ VectorDrawableAnimatorUI(@NonNull AnimatedVectorDrawable drawable) {
+ mDrawable = drawable;
+ }
+
+ @Override
+ public void init(@NonNull AnimatorSet set) {
+ if (mSet != null) {
+ // Already initialized
+ throw new UnsupportedOperationException("VectorDrawableAnimator cannot be " +
+ "re-initialized");
+ }
+ // Keep a deep copy of the set, such that set can be still be constantly representing
+ // the static content from XML file.
+ mSet = set.clone();
+ mIsInfinite = mSet.getTotalDuration() == Animator.DURATION_INFINITE;
+
+ // If there are listeners added before calling init(), now they should be setup.
+ if (mListenerArray != null && !mListenerArray.isEmpty()) {
+ for (int i = 0; i < mListenerArray.size(); i++) {
+ mSet.addListener(mListenerArray.get(i));
+ }
+ mListenerArray.clear();
+ mListenerArray = null;
+ }
+ }
+
+ // Although start(), reset() and reverse() should call init() already, it is better to
+ // protect these functions from NPE in any situation.
+ @Override
+ public void start() {
+ if (mSet == null || mSet.isStarted()) {
+ return;
+ }
+ mSet.start();
+ invalidateOwningView();
+ }
+
+ @Override
+ public void end() {
+ if (mSet == null) {
+ return;
+ }
+ mSet.end();
+ }
+
+ @Override
+ public void reset() {
+ if (mSet == null) {
+ return;
+ }
+ start();
+ mSet.cancel();
+ }
+
+ @Override
+ public void reverse() {
+ if (mSet == null) {
+ return;
+ }
+ mSet.reverse();
+ invalidateOwningView();
+ }
+
+ @Override
+ public boolean canReverse() {
+ return mSet != null && mSet.canReverse();
+ }
+
+ @Override
+ public void setListener(AnimatorListener listener) {
+ if (mSet == null) {
+ if (mListenerArray == null) {
+ mListenerArray = new ArrayList<AnimatorListener>();
+ }
+ mListenerArray.add(listener);
+ } else {
+ mSet.addListener(listener);
+ }
+ }
+
+ @Override
+ public void removeListener(AnimatorListener listener) {
+ if (mSet == null) {
+ if (mListenerArray == null) {
+ return;
+ }
+ mListenerArray.remove(listener);
+ } else {
+ mSet.removeListener(listener);
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (mSet != null && mSet.isStarted()) {
+ invalidateOwningView();
+ }
+ }
+
+ @Override
+ public boolean isStarted() {
+ return mSet != null && mSet.isStarted();
+ }
+
+ @Override
+ public boolean isRunning() {
+ return mSet != null && mSet.isRunning();
+ }
+
+ @Override
+ public boolean isInfinite() {
+ return mIsInfinite;
+ }
+
+ @Override
+ public void pause() {
+ if (mSet == null) {
+ return;
+ }
+ mSet.pause();
+ }
+
+ @Override
+ public void resume() {
+ if (mSet == null) {
+ return;
+ }
+ mSet.resume();
+ }
+
+ private void invalidateOwningView() {
+ mDrawable.invalidateSelf();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static class VectorDrawableAnimatorRT implements VectorDrawableAnimator {
+ private static final int START_ANIMATION = 1;
+ private static final int REVERSE_ANIMATION = 2;
+ private static final int RESET_ANIMATION = 3;
+ private static final int END_ANIMATION = 4;
+
+ // If the duration of an animation is more than 300 frames, we cap the sample size to 300.
+ private static final int MAX_SAMPLE_POINTS = 300;
+ private AnimatorListener mListener = null;
+ private final LongArray mStartDelays = new LongArray();
+ private PropertyValuesHolder.PropertyValues mTmpValues =
+ new PropertyValuesHolder.PropertyValues();
+ private long mSetPtr = 0;
+ private boolean mContainsSequentialAnimators = false;
+ private boolean mStarted = false;
+ private boolean mInitialized = false;
+ private boolean mIsReversible = false;
+ private boolean mIsInfinite = false;
+ // TODO: Consider using NativeAllocationRegistery to track native allocation
+ private final VirtualRefBasePtr mSetRefBasePtr;
+ private WeakReference<RenderNode> mLastSeenTarget = null;
+ private int mLastListenerId = 0;
+ private final IntArray mPendingAnimationActions = new IntArray();
+ private final AnimatedVectorDrawable mDrawable;
+
+ VectorDrawableAnimatorRT(AnimatedVectorDrawable drawable) {
+ mDrawable = drawable;
+ mSetPtr = nCreateAnimatorSet();
+ // Increment ref count on native AnimatorSet, so it doesn't get released before Java
+ // side is done using it.
+ mSetRefBasePtr = new VirtualRefBasePtr(mSetPtr);
+ }
+
+ @Override
+ public void init(@NonNull AnimatorSet set) {
+ if (mInitialized) {
+ // Already initialized
+ throw new UnsupportedOperationException("VectorDrawableAnimator cannot be " +
+ "re-initialized");
+ }
+ parseAnimatorSet(set, 0);
+ long vectorDrawableTreePtr = mDrawable.mAnimatedVectorState.mVectorDrawable
+ .getNativeTree();
+ nSetVectorDrawableTarget(mSetPtr, vectorDrawableTreePtr);
+ mInitialized = true;
+ mIsInfinite = set.getTotalDuration() == Animator.DURATION_INFINITE;
+
+ // Check reversible.
+ mIsReversible = true;
+ if (mContainsSequentialAnimators) {
+ mIsReversible = false;
+ } else {
+ // Check if there's any start delay set on child
+ for (int i = 0; i < mStartDelays.size(); i++) {
+ if (mStartDelays.get(i) > 0) {
+ mIsReversible = false;
+ return;
+ }
+ }
+ }
+ }
+
+ private void parseAnimatorSet(AnimatorSet set, long startTime) {
+ ArrayList<Animator> animators = set.getChildAnimations();
+
+ boolean playTogether = set.shouldPlayTogether();
+ // Convert AnimatorSet to VectorDrawableAnimatorRT
+ for (int i = 0; i < animators.size(); i++) {
+ Animator animator = animators.get(i);
+ // Here we only support ObjectAnimator
+ if (animator instanceof AnimatorSet) {
+ parseAnimatorSet((AnimatorSet) animator, startTime);
+ } else if (animator instanceof ObjectAnimator) {
+ createRTAnimator((ObjectAnimator) animator, startTime);
+ } // ignore ValueAnimators and others because they don't directly modify VD
+ // therefore will be useless to AVD.
+
+ if (!playTogether) {
+ // Assume not play together means play sequentially
+ startTime += animator.getTotalDuration();
+ mContainsSequentialAnimators = true;
+ }
+ }
+ }
+
+ // TODO: This method reads animation data from already parsed Animators. We need to move
+ // this step further up the chain in the parser to avoid the detour.
+ private void createRTAnimator(ObjectAnimator animator, long startTime) {
+ PropertyValuesHolder[] values = animator.getValues();
+ Object target = animator.getTarget();
+ if (target instanceof VectorDrawable.VGroup) {
+ createRTAnimatorForGroup(values, animator, (VectorDrawable.VGroup) target,
+ startTime);
+ } else if (target instanceof VectorDrawable.VPath) {
+ for (int i = 0; i < values.length; i++) {
+ values[i].getPropertyValues(mTmpValues);
+ if (mTmpValues.endValue instanceof PathParser.PathData &&
+ mTmpValues.propertyName.equals("pathData")) {
+ createRTAnimatorForPath(animator, (VectorDrawable.VPath) target,
+ startTime);
+ } else if (target instanceof VectorDrawable.VFullPath) {
+ createRTAnimatorForFullPath(animator, (VectorDrawable.VFullPath) target,
+ startTime);
+ } else if (!mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
+ throw new IllegalArgumentException("ClipPath only supports PathData " +
+ "property");
+ }
+ }
+ } else if (target instanceof VectorDrawable.VectorDrawableState) {
+ createRTAnimatorForRootGroup(values, animator,
+ (VectorDrawable.VectorDrawableState) target, startTime);
+ }
+ }
+
+ private void createRTAnimatorForGroup(PropertyValuesHolder[] values,
+ ObjectAnimator animator, VectorDrawable.VGroup target,
+ long startTime) {
+
+ long nativePtr = target.getNativePtr();
+ int propertyId;
+ for (int i = 0; i < values.length; i++) {
+ // TODO: We need to support the rare case in AVD where no start value is provided
+ values[i].getPropertyValues(mTmpValues);
+ propertyId = VectorDrawable.VGroup.getPropertyIndex(mTmpValues.propertyName);
+ if (mTmpValues.type != Float.class && mTmpValues.type != float.class) {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.e(LOGTAG, "Unsupported type: " +
+ mTmpValues.type + ". Only float value is supported for Groups.");
+ }
+ continue;
+ }
+ if (propertyId < 0) {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.e(LOGTAG, "Unsupported property: " +
+ mTmpValues.propertyName + " for Vector Drawable Group");
+ }
+ continue;
+ }
+ long propertyPtr = nCreateGroupPropertyHolder(nativePtr, propertyId,
+ (Float) mTmpValues.startValue, (Float) mTmpValues.endValue);
+ if (mTmpValues.dataSource != null) {
+ float[] dataPoints = createFloatDataPoints(mTmpValues.dataSource,
+ animator.getDuration());
+ nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
+ }
+ createNativeChildAnimator(propertyPtr, startTime, animator);
+ }
+ }
+ private void createRTAnimatorForPath( ObjectAnimator animator, VectorDrawable.VPath target,
+ long startTime) {
+
+ long nativePtr = target.getNativePtr();
+ long startPathDataPtr = ((PathParser.PathData) mTmpValues.startValue)
+ .getNativePtr();
+ long endPathDataPtr = ((PathParser.PathData) mTmpValues.endValue)
+ .getNativePtr();
+ long propertyPtr = nCreatePathDataPropertyHolder(nativePtr, startPathDataPtr,
+ endPathDataPtr);
+ createNativeChildAnimator(propertyPtr, startTime, animator);
+ }
+
+ private void createRTAnimatorForFullPath(ObjectAnimator animator,
+ VectorDrawable.VFullPath target, long startTime) {
+
+ int propertyId = target.getPropertyIndex(mTmpValues.propertyName);
+ long propertyPtr;
+ long nativePtr = target.getNativePtr();
+ if (mTmpValues.type == Float.class || mTmpValues.type == float.class) {
+ if (propertyId < 0) {
+ if (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
+ return;
+ } else {
+ throw new IllegalArgumentException("Property: " + mTmpValues.propertyName
+ + " is not supported for FullPath");
+ }
+ }
+ propertyPtr = nCreatePathPropertyHolder(nativePtr, propertyId,
+ (Float) mTmpValues.startValue, (Float) mTmpValues.endValue);
+ if (mTmpValues.dataSource != null) {
+ // Pass keyframe data to native, if any.
+ float[] dataPoints = createFloatDataPoints(mTmpValues.dataSource,
+ animator.getDuration());
+ nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
+ }
+
+ } else if (mTmpValues.type == Integer.class || mTmpValues.type == int.class) {
+ propertyPtr = nCreatePathColorPropertyHolder(nativePtr, propertyId,
+ (Integer) mTmpValues.startValue, (Integer) mTmpValues.endValue);
+ if (mTmpValues.dataSource != null) {
+ // Pass keyframe data to native, if any.
+ int[] dataPoints = createIntDataPoints(mTmpValues.dataSource,
+ animator.getDuration());
+ nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
+ }
+ } else {
+ if (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
+ return;
+ } else {
+ throw new UnsupportedOperationException("Unsupported type: " +
+ mTmpValues.type + ". Only float, int or PathData value is " +
+ "supported for Paths.");
+ }
+ }
+ createNativeChildAnimator(propertyPtr, startTime, animator);
+ }
+
+ private void createRTAnimatorForRootGroup(PropertyValuesHolder[] values,
+ ObjectAnimator animator, VectorDrawable.VectorDrawableState target,
+ long startTime) {
+ long nativePtr = target.getNativeRenderer();
+ if (!animator.getPropertyName().equals("alpha")) {
+ if (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
+ return;
+ } else {
+ throw new UnsupportedOperationException("Only alpha is supported for root "
+ + "group");
+ }
+ }
+ Float startValue = null;
+ Float endValue = null;
+ for (int i = 0; i < values.length; i++) {
+ values[i].getPropertyValues(mTmpValues);
+ if (mTmpValues.propertyName.equals("alpha")) {
+ startValue = (Float) mTmpValues.startValue;
+ endValue = (Float) mTmpValues.endValue;
+ break;
+ }
+ }
+ if (startValue == null && endValue == null) {
+ if (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
+ return;
+ } else {
+ throw new UnsupportedOperationException("No alpha values are specified");
+ }
+ }
+ long propertyPtr = nCreateRootAlphaPropertyHolder(nativePtr, startValue, endValue);
+ if (mTmpValues.dataSource != null) {
+ // Pass keyframe data to native, if any.
+ float[] dataPoints = createFloatDataPoints(mTmpValues.dataSource,
+ animator.getDuration());
+ nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
+ }
+ createNativeChildAnimator(propertyPtr, startTime, animator);
+ }
+
+ /**
+ * Calculate the amount of frames an animation will run based on duration.
+ */
+ private static int getFrameCount(long duration) {
+ long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos();
+ int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS);
+ int numAnimFrames = (int) Math.ceil(((double) duration) / animIntervalMs);
+ // We need 2 frames of data minimum.
+ numAnimFrames = Math.max(2, numAnimFrames);
+ if (numAnimFrames > MAX_SAMPLE_POINTS) {
+ Log.w("AnimatedVectorDrawable", "Duration for the animation is too long :" +
+ duration + ", the animation will subsample the keyframe or path data.");
+ numAnimFrames = MAX_SAMPLE_POINTS;
+ }
+ return numAnimFrames;
+ }
+
+ // These are the data points that define the value of the animating properties.
+ // e.g. translateX and translateY can animate along a Path, at any fraction in [0, 1]
+ // a point on the path corresponds to the values of translateX and translateY.
+ // TODO: (Optimization) We should pass the path down in native and chop it into segments
+ // in native.
+ private static float[] createFloatDataPoints(
+ PropertyValuesHolder.PropertyValues.DataSource dataSource, long duration) {
+ int numAnimFrames = getFrameCount(duration);
+ float values[] = new float[numAnimFrames];
+ float lastFrame = numAnimFrames - 1;
+ for (int i = 0; i < numAnimFrames; i++) {
+ float fraction = i / lastFrame;
+ values[i] = (Float) dataSource.getValueAtFraction(fraction);
+ }
+ return values;
+ }
+
+ private static int[] createIntDataPoints(
+ PropertyValuesHolder.PropertyValues.DataSource dataSource, long duration) {
+ int numAnimFrames = getFrameCount(duration);
+ int values[] = new int[numAnimFrames];
+ float lastFrame = numAnimFrames - 1;
+ for (int i = 0; i < numAnimFrames; i++) {
+ float fraction = i / lastFrame;
+ values[i] = (Integer) dataSource.getValueAtFraction(fraction);
+ }
+ return values;
+ }
+
+ private void createNativeChildAnimator(long propertyPtr, long extraDelay,
+ ObjectAnimator animator) {
+ long duration = animator.getDuration();
+ int repeatCount = animator.getRepeatCount();
+ long startDelay = extraDelay + animator.getStartDelay();
+ TimeInterpolator interpolator = animator.getInterpolator();
+ long nativeInterpolator =
+ RenderNodeAnimatorSetHelper.createNativeInterpolator(interpolator, duration);
+
+ startDelay *= ValueAnimator.getDurationScale();
+ duration *= ValueAnimator.getDurationScale();
+
+ mStartDelays.add(startDelay);
+ nAddAnimator(mSetPtr, propertyPtr, nativeInterpolator, startDelay, duration,
+ repeatCount, animator.getRepeatMode());
+ }
+
+ /**
+ * Holds a weak reference to the target that was last seen (through the DisplayListCanvas
+ * in the last draw call), so that when animator set needs to start, we can add the animator
+ * to the last seen RenderNode target and start right away.
+ */
+ protected void recordLastSeenTarget(DisplayListCanvas canvas) {
+ final RenderNode node = RenderNodeAnimatorSetHelper.getTarget(canvas);
+ mLastSeenTarget = new WeakReference<RenderNode>(node);
+ // Add the animator to the list of animators on every draw
+ if (mInitialized || mPendingAnimationActions.size() > 0) {
+ if (useTarget(node)) {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.d(LOGTAG, "Target is set in the next frame");
+ }
+ for (int i = 0; i < mPendingAnimationActions.size(); i++) {
+ handlePendingAction(mPendingAnimationActions.get(i));
+ }
+ mPendingAnimationActions.clear();
+ }
+ }
+ }
+
+ private void handlePendingAction(int pendingAnimationAction) {
+ if (pendingAnimationAction == START_ANIMATION) {
+ startAnimation();
+ } else if (pendingAnimationAction == REVERSE_ANIMATION) {
+ reverseAnimation();
+ } else if (pendingAnimationAction == RESET_ANIMATION) {
+ resetAnimation();
+ } else if (pendingAnimationAction == END_ANIMATION) {
+ endAnimation();
+ } else {
+ throw new UnsupportedOperationException("Animation action " +
+ pendingAnimationAction + "is not supported");
+ }
+ }
+
+ private boolean useLastSeenTarget() {
+ if (mLastSeenTarget != null) {
+ final RenderNode target = mLastSeenTarget.get();
+ return useTarget(target);
+ }
+ return false;
+ }
+
+ private boolean useTarget(RenderNode target) {
+ if (target != null && target.isAttached()) {
+ target.registerVectorDrawableAnimator(this);
+ return true;
+ }
+ return false;
+ }
+
+ private void invalidateOwningView() {
+ mDrawable.invalidateSelf();
+ }
+
+ private void addPendingAction(int pendingAnimationAction) {
+ invalidateOwningView();
+ mPendingAnimationActions.add(pendingAnimationAction);
+ }
+
+ @Override
+ public void start() {
+ if (!mInitialized) {
+ return;
+ }
+
+ if (useLastSeenTarget()) {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.d(LOGTAG, "Target is set. Starting VDAnimatorSet from java");
+ }
+ startAnimation();
+ } else {
+ addPendingAction(START_ANIMATION);
+ }
+ }
+
+ @Override
+ public void end() {
+ if (!mInitialized) {
+ return;
+ }
+
+ if (useLastSeenTarget()) {
+ endAnimation();
+ } else {
+ addPendingAction(END_ANIMATION);
+ }
+ }
+
+ @Override
+ public void reset() {
+ if (!mInitialized) {
+ return;
+ }
+
+ if (useLastSeenTarget()) {
+ resetAnimation();
+ } else {
+ addPendingAction(RESET_ANIMATION);
+ }
+ }
+
+ // Current (imperfect) Java AnimatorSet cannot be reversed when the set contains sequential
+ // animators or when the animator set has a start delay
+ @Override
+ public void reverse() {
+ if (!mIsReversible || !mInitialized) {
+ return;
+ }
+ if (useLastSeenTarget()) {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.d(LOGTAG, "Target is set. Reversing VDAnimatorSet from java");
+ }
+ reverseAnimation();
+ } else {
+ addPendingAction(REVERSE_ANIMATION);
+ }
+ }
+
+ // This should only be called after animator has been added to the RenderNode target.
+ private void startAnimation() {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.w(LOGTAG, "starting animation on VD: " +
+ ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState)
+ mDrawable.getConstantState()).mVectorDrawable.getConstantState())
+ .mRootName);
+ }
+ mStarted = true;
+ nStart(mSetPtr, this, ++mLastListenerId);
+ invalidateOwningView();
+ if (mListener != null) {
+ mListener.onAnimationStart(null);
+ }
+ }
+
+ // This should only be called after animator has been added to the RenderNode target.
+ private void endAnimation() {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.w(LOGTAG, "ending animation on VD: " +
+ ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState)
+ mDrawable.getConstantState()).mVectorDrawable.getConstantState())
+ .mRootName);
+ }
+ nEnd(mSetPtr);
+ invalidateOwningView();
+ }
+
+ // This should only be called after animator has been added to the RenderNode target.
+ private void resetAnimation() {
+ nReset(mSetPtr);
+ invalidateOwningView();
+ }
+
+ // This should only be called after animator has been added to the RenderNode target.
+ private void reverseAnimation() {
+ mStarted = true;
+ nReverse(mSetPtr, this, ++mLastListenerId);
+ invalidateOwningView();
+ if (mListener != null) {
+ mListener.onAnimationStart(null);
+ }
+ }
+
+ public long getAnimatorNativePtr() {
+ return mSetPtr;
+ }
+
+ @Override
+ public boolean canReverse() {
+ return mIsReversible;
+ }
+
+ @Override
+ public boolean isStarted() {
+ return mStarted;
+ }
+
+ @Override
+ public boolean isRunning() {
+ if (!mInitialized) {
+ return false;
+ }
+ return mStarted;
+ }
+
+ @Override
+ public void setListener(AnimatorListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void removeListener(AnimatorListener listener) {
+ mListener = null;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (canvas.isHardwareAccelerated()) {
+ recordLastSeenTarget((DisplayListCanvas) canvas);
+ }
+ }
+
+ @Override
+ public boolean isInfinite() {
+ return mIsInfinite;
+ }
+
+ @Override
+ public void pause() {
+ // TODO: Implement pause for Animator On RT.
+ }
+
+ @Override
+ public void resume() {
+ // TODO: Implement resume for Animator On RT.
+ }
+
+ private void onAnimationEnd(int listenerId) {
+ if (listenerId != mLastListenerId) {
+ return;
+ }
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.d(LOGTAG, "on finished called from native");
+ }
+ mStarted = false;
+ // Invalidate in the end of the animation to make sure the data in
+ // RT thread is synced back to UI thread.
+ invalidateOwningView();
+ if (mListener != null) {
+ mListener.onAnimationEnd(null);
+ }
+ }
+
+ // onFinished: should be called from native
+ private static void callOnFinished(VectorDrawableAnimatorRT set, int id) {
+ set.onAnimationEnd(id);
+ }
+
+ private void transferPendingActions(VectorDrawableAnimator animatorSet) {
+ for (int i = 0; i < mPendingAnimationActions.size(); i++) {
+ int pendingAction = mPendingAnimationActions.get(i);
+ if (pendingAction == START_ANIMATION) {
+ animatorSet.start();
+ } else if (pendingAction == END_ANIMATION) {
+ animatorSet.end();
+ } else if (pendingAction == REVERSE_ANIMATION) {
+ animatorSet.reverse();
+ } else if (pendingAction == RESET_ANIMATION) {
+ animatorSet.reset();
+ } else {
+ throw new UnsupportedOperationException("Animation action " +
+ pendingAction + "is not supported");
+ }
+ }
+ mPendingAnimationActions.clear();
+ }
+ }
+
+ private static native long nCreateAnimatorSet();
+ private static native void nSetVectorDrawableTarget(long animatorPtr, long vectorDrawablePtr);
+ private static native void nAddAnimator(long setPtr, long propertyValuesHolder,
+ long nativeInterpolator, long startDelay, long duration, int repeatCount,
+ int repeatMode);
+ private static native void nSetPropertyHolderData(long nativePtr, float[] data, int length);
+ private static native void nSetPropertyHolderData(long nativePtr, int[] data, int length);
+ private static native void nStart(long animatorSetPtr, VectorDrawableAnimatorRT set, int id);
+ private static native void nReverse(long animatorSetPtr, VectorDrawableAnimatorRT set, int id);
+
+ // ------------- @FastNative -------------------
+
+ @FastNative
+ private static native long nCreateGroupPropertyHolder(long nativePtr, int propertyId,
+ float startValue, float endValue);
+ @FastNative
+ private static native long nCreatePathDataPropertyHolder(long nativePtr, long startValuePtr,
+ long endValuePtr);
+ @FastNative
+ private static native long nCreatePathColorPropertyHolder(long nativePtr, int propertyId,
+ int startValue, int endValue);
+ @FastNative
+ private static native long nCreatePathPropertyHolder(long nativePtr, int propertyId,
+ float startValue, float endValue);
+ @FastNative
+ private static native long nCreateRootAlphaPropertyHolder(long nativePtr, float startValue,
+ float endValue);
+ @FastNative
+ private static native void nEnd(long animatorSetPtr);
+ @FastNative
+ private static native void nReset(long animatorSetPtr);
+}
diff --git a/android/graphics/drawable/AnimatedVectorDrawable_Delegate.java b/android/graphics/drawable/AnimatedVectorDrawable_Delegate.java
new file mode 100644
index 0000000..ad2c564
--- /dev/null
+++ b/android/graphics/drawable/AnimatedVectorDrawable_Delegate.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.drawable;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.internal.view.animation.NativeInterpolatorFactoryHelper_Delegate;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.drawable.AnimatedVectorDrawable.VectorDrawableAnimatorRT;
+import android.graphics.drawable.VectorDrawable_Delegate.VFullPath_Delegate;
+import android.graphics.drawable.VectorDrawable_Delegate.VGroup_Delegate;
+import android.graphics.drawable.VectorDrawable_Delegate.VNativeObject;
+import android.graphics.drawable.VectorDrawable_Delegate.VPathRenderer_Delegate;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link
+ * AnimatedVectorDrawable}
+ * <p>
+ * Through the layoutlib_create tool, the original methods of AnimatedVectorDrawable have been
+ * replaced by calls to methods of the same name in this delegate class.
+ */
+@SuppressWarnings("unused")
+public class AnimatedVectorDrawable_Delegate {
+ private static DelegateManager<AnimatorSetHolder> sAnimatorSets = new
+ DelegateManager<>(AnimatorSetHolder.class);
+ private static DelegateManager<PropertySetter> sHolders = new
+ DelegateManager<>(PropertySetter.class);
+
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreateAnimatorSet() {
+ return sAnimatorSets.addNewDelegate(new AnimatorSetHolder());
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetVectorDrawableTarget(long animatorPtr, long vectorDrawablePtr) {
+ // TODO: implement
+ }
+ @LayoutlibDelegate
+ /*package*/ static void nAddAnimator(long setPtr, long propertyValuesHolder,
+ long nativeInterpolator, long startDelay, long duration, int repeatCount,
+ int repeatMode) {
+ PropertySetter holder = sHolders.getDelegate(propertyValuesHolder);
+ if (holder == null || holder.getValues() == null) {
+ return;
+ }
+
+ ObjectAnimator animator = new ObjectAnimator();
+ animator.setValues(holder.getValues());
+ animator.setInterpolator(
+ NativeInterpolatorFactoryHelper_Delegate.getDelegate(nativeInterpolator));
+ animator.setStartDelay(startDelay);
+ animator.setDuration(duration);
+ animator.setRepeatCount(repeatCount);
+ animator.setRepeatMode(repeatMode);
+ animator.setTarget(holder);
+ animator.setPropertyName(holder.getValues().getPropertyName());
+
+ AnimatorSetHolder set = sAnimatorSets.getDelegate(setPtr);
+ assert set != null;
+ set.addAnimator(animator);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreateGroupPropertyHolder(long nativePtr, int propertyId,
+ float startValue, float endValue) {
+ VGroup_Delegate group = VNativeObject.getDelegate(nativePtr);
+ Consumer<Float> setter = group.getPropertySetter(propertyId);
+
+ return sHolders.addNewDelegate(FloatPropertySetter.of(setter, startValue,
+ endValue));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreatePathDataPropertyHolder(long nativePtr, long startValuePtr,
+ long endValuePtr) {
+ Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, "AnimatedVectorDrawable path " +
+ "animations are not supported.", null, null);
+ return 0;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreatePathColorPropertyHolder(long nativePtr, int propertyId,
+ int startValue, int endValue) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(nativePtr);
+ Consumer<Integer> setter = path.getIntPropertySetter(propertyId);
+
+ return sHolders.addNewDelegate(IntPropertySetter.of(setter, startValue,
+ endValue));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreatePathPropertyHolder(long nativePtr, int propertyId,
+ float startValue, float endValue) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(nativePtr);
+ Consumer<Float> setter = path.getFloatPropertySetter(propertyId);
+
+ return sHolders.addNewDelegate(FloatPropertySetter.of(setter, startValue,
+ endValue));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreateRootAlphaPropertyHolder(long nativePtr, float startValue,
+ float endValue) {
+ VPathRenderer_Delegate renderer = VNativeObject.getDelegate(nativePtr);
+
+ return sHolders.addNewDelegate(FloatPropertySetter.of(renderer::setRootAlpha,
+ startValue,
+ endValue));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetPropertyHolderData(long nativePtr, float[] data, int length) {
+ PropertySetter setter = sHolders.getDelegate(nativePtr);
+ assert setter != null;
+
+ setter.setValues(data);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetPropertyHolderData(long nativePtr, int[] data, int length) {
+ PropertySetter setter = sHolders.getDelegate(nativePtr);
+ assert setter != null;
+
+ setter.setValues(data);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nStart(long animatorSetPtr, VectorDrawableAnimatorRT set, int id) {
+ AnimatorSetHolder animatorSet = sAnimatorSets.getDelegate(animatorSetPtr);
+ assert animatorSet != null;
+
+ animatorSet.start();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nReverse(long animatorSetPtr, VectorDrawableAnimatorRT set, int id) {
+ AnimatorSetHolder animatorSet = sAnimatorSets.getDelegate(animatorSetPtr);
+ assert animatorSet != null;
+
+ animatorSet.reverse();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nEnd(long animatorSetPtr) {
+ AnimatorSetHolder animatorSet = sAnimatorSets.getDelegate(animatorSetPtr);
+ assert animatorSet != null;
+
+ animatorSet.end();
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nReset(long animatorSetPtr) {
+ AnimatorSetHolder animatorSet = sAnimatorSets.getDelegate(animatorSetPtr);
+ assert animatorSet != null;
+
+ animatorSet.end();
+ animatorSet.start();
+ }
+
+ private static class AnimatorSetHolder {
+ private ArrayList<Animator> mAnimators = new ArrayList<>();
+ private AnimatorSet mAnimatorSet = null;
+
+ private void addAnimator(@NonNull Animator animator) {
+ mAnimators.add(animator);
+ }
+
+ private void ensureAnimatorSet() {
+ if (mAnimatorSet == null) {
+ mAnimatorSet = new AnimatorSet();
+ mAnimatorSet.playTogether(mAnimators);
+ }
+ }
+
+ private void start() {
+ ensureAnimatorSet();
+
+ mAnimatorSet.start();
+ }
+
+ private void end() {
+ mAnimatorSet.end();
+ }
+
+ private void reset() {
+ end();
+ start();
+ }
+
+ private void reverse() {
+ mAnimatorSet.reverse();
+ }
+ }
+
+ /**
+ * Class that allows setting a value and holds the range of values for the given property.
+ *
+ * @param <T> the type of the property
+ */
+ private static class PropertySetter<T> {
+ final Consumer<T> mValueSetter;
+ private PropertyValuesHolder mValues;
+
+ private PropertySetter(@NonNull Consumer<T> valueSetter) {
+ mValueSetter = valueSetter;
+ }
+
+ /**
+ * Method to set an {@link Integer} value for this property. The default implementation of
+ * this method doesn't do anything. This method is accessed via reflection by the
+ * PropertyValuesHolder.
+ */
+ public void setIntValue(Integer value) {
+ }
+
+ /**
+ * Method to set an {@link Integer} value for this property. The default implementation of
+ * this method doesn't do anything. This method is accessed via reflection by the
+ * PropertyValuesHolder.
+ */
+ public void setFloatValue(Float value) {
+ }
+
+ void setValues(float... values) {
+ mValues = PropertyValuesHolder.ofFloat("floatValue", values);
+ }
+
+ @Nullable
+ PropertyValuesHolder getValues() {
+ return mValues;
+ }
+
+ void setValues(int... values) {
+ mValues = PropertyValuesHolder.ofInt("intValue", values);
+ }
+ }
+
+ private static class IntPropertySetter extends PropertySetter<Integer> {
+ private IntPropertySetter(Consumer<Integer> valueSetter) {
+ super(valueSetter);
+ }
+
+ private static PropertySetter of(Consumer<Integer> valueSetter, int... values) {
+ PropertySetter setter = new IntPropertySetter(valueSetter);
+ setter.setValues(values);
+
+ return setter;
+ }
+
+ public void setIntValue(Integer value) {
+ mValueSetter.accept(value);
+ }
+ }
+
+ private static class FloatPropertySetter extends PropertySetter<Float> {
+ private FloatPropertySetter(Consumer<Float> valueSetter) {
+ super(valueSetter);
+ }
+
+ private static PropertySetter of(Consumer<Float> valueSetter, float... values) {
+ PropertySetter setter = new FloatPropertySetter(valueSetter);
+ setter.setValues(values);
+
+ return setter;
+ }
+
+ public void setFloatValue(Float value) {
+ mValueSetter.accept(value);
+ }
+
+ }
+}
diff --git a/android/graphics/drawable/AnimatedVectorDrawable_VectorDrawableAnimatorRT_Delegate.java b/android/graphics/drawable/AnimatedVectorDrawable_VectorDrawableAnimatorRT_Delegate.java
new file mode 100644
index 0000000..fc848d9
--- /dev/null
+++ b/android/graphics/drawable/AnimatedVectorDrawable_VectorDrawableAnimatorRT_Delegate.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.drawable;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.graphics.Canvas;
+import android.graphics.drawable.AnimatedVectorDrawable.VectorDrawableAnimatorRT;
+
+public class AnimatedVectorDrawable_VectorDrawableAnimatorRT_Delegate {
+ @LayoutlibDelegate
+ /*package*/ static boolean useLastSeenTarget(VectorDrawableAnimatorRT thisDrawableAnimator) {
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void onDraw(VectorDrawableAnimatorRT thisDrawableAnimator, Canvas canvas) {
+ // Do not attempt to record as we are not using a DisplayListCanvas
+ }
+}
diff --git a/android/graphics/drawable/AnimationDrawable.java b/android/graphics/drawable/AnimationDrawable.java
new file mode 100644
index 0000000..0fd1741
--- /dev/null
+++ b/android/graphics/drawable/AnimationDrawable.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import com.android.internal.R;
+
+import java.io.IOException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.NonNull;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.Resources.Theme;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+
+/**
+ * An object used to create frame-by-frame animations, defined by a series of
+ * Drawable objects, which can be used as a View object's background.
+ * <p>
+ * The simplest way to create a frame-by-frame animation is to define the
+ * animation in an XML file, placed in the res/drawable/ folder, and set it as
+ * the background to a View object. Then, call {@link #start()} to run the
+ * animation.
+ * <p>
+ * An AnimationDrawable defined in XML consists of a single
+ * {@code <animation-list>} element and a series of nested
+ * {@code <item>} tags. Each item defines a frame of the animation. See
+ * the example below.
+ * <p>
+ * spin_animation.xml file in res/drawable/ folder:
+ * <pre>
+ * <!-- Animation frames are wheel0.png through wheel5.png
+ * files inside the res/drawable/ folder -->
+ * <animation-list android:id="@+id/selected" android:oneshot="false">
+ * <item android:drawable="@drawable/wheel0" android:duration="50" />
+ * <item android:drawable="@drawable/wheel1" android:duration="50" />
+ * <item android:drawable="@drawable/wheel2" android:duration="50" />
+ * <item android:drawable="@drawable/wheel3" android:duration="50" />
+ * <item android:drawable="@drawable/wheel4" android:duration="50" />
+ * <item android:drawable="@drawable/wheel5" android:duration="50" />
+ * </animation-list></pre>
+ * <p>
+ * Here is the code to load and play this animation.
+ * <pre>
+ * // Load the ImageView that will host the animation and
+ * // set its background to our AnimationDrawable XML resource.
+ * ImageView img = (ImageView)findViewById(R.id.spinning_wheel_image);
+ * img.setBackgroundResource(R.drawable.spin_animation);
+ *
+ * // Get the background, which has been compiled to an AnimationDrawable object.
+ * AnimationDrawable frameAnimation = (AnimationDrawable) img.getBackground();
+ *
+ * // Start the animation (looped playback by default).
+ * frameAnimation.start();
+ * </pre>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about animating with {@code AnimationDrawable}, read the
+ * <a href="{@docRoot}guide/topics/graphics/drawable-animation.html">Drawable Animation</a>
+ * developer guide.</p>
+ * </div>
+ *
+ * @attr ref android.R.styleable#AnimationDrawable_visible
+ * @attr ref android.R.styleable#AnimationDrawable_variablePadding
+ * @attr ref android.R.styleable#AnimationDrawable_oneshot
+ * @attr ref android.R.styleable#AnimationDrawableItem_duration
+ * @attr ref android.R.styleable#AnimationDrawableItem_drawable
+ */
+public class AnimationDrawable extends DrawableContainer implements Runnable, Animatable {
+ private AnimationState mAnimationState;
+
+ /** The current frame, ranging from 0 to {@link #mAnimationState#getChildCount() - 1} */
+ private int mCurFrame = 0;
+
+ /** Whether the drawable has an animation callback posted. */
+ private boolean mRunning;
+
+ /** Whether the drawable should animate when visible. */
+ private boolean mAnimating;
+
+ private boolean mMutated;
+
+ public AnimationDrawable() {
+ this(null, null);
+ }
+
+ /**
+ * Sets whether this AnimationDrawable is visible.
+ * <p>
+ * When the drawable becomes invisible, it will pause its animation. A subsequent change to
+ * visible with <code>restart</code> set to true will restart the animation from the
+ * first frame. If <code>restart</code> is false, the drawable will resume from the most recent
+ * frame. If the drawable has already reached the last frame, it will then loop back to the
+ * first frame, unless it's a one shot drawable (set through {@link #setOneShot(boolean)}),
+ * in which case, it will stay on the last frame.
+ *
+ * @param visible true if visible, false otherwise
+ * @param restart when visible, true to force the animation to restart
+ * from the first frame
+ * @return true if the new visibility is different than its previous state
+ */
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ final boolean changed = super.setVisible(visible, restart);
+ if (visible) {
+ if (restart || changed) {
+ boolean startFromZero = restart || (!mRunning && !mAnimationState.mOneShot) ||
+ mCurFrame >= mAnimationState.getChildCount();
+ setFrame(startFromZero ? 0 : mCurFrame, true, mAnimating);
+ }
+ } else {
+ unscheduleSelf(this);
+ }
+ return changed;
+ }
+
+ /**
+ * Starts the animation from the first frame, looping if necessary. This method has no effect
+ * if the animation is running.
+ * <p>
+ * <strong>Note:</strong> Do not call this in the
+ * {@link android.app.Activity#onCreate} method of your activity, because
+ * the {@link AnimationDrawable} is not yet fully attached to the window.
+ * If you want to play the animation immediately without requiring
+ * interaction, then you might want to call it from the
+ * {@link android.app.Activity#onWindowFocusChanged} method in your
+ * activity, which will get called when Android brings your window into
+ * focus.
+ *
+ * @see #isRunning()
+ * @see #stop()
+ */
+ @Override
+ public void start() {
+ mAnimating = true;
+
+ if (!isRunning()) {
+ // Start from 0th frame.
+ setFrame(0, false, mAnimationState.getChildCount() > 1
+ || !mAnimationState.mOneShot);
+ }
+ }
+
+ /**
+ * Stops the animation at the current frame. This method has no effect if the animation is not
+ * running.
+ *
+ * @see #isRunning()
+ * @see #start()
+ */
+ @Override
+ public void stop() {
+ mAnimating = false;
+
+ if (isRunning()) {
+ mCurFrame = 0;
+ unscheduleSelf(this);
+ }
+ }
+
+ /**
+ * Indicates whether the animation is currently running or not.
+ *
+ * @return true if the animation is running, false otherwise
+ */
+ @Override
+ public boolean isRunning() {
+ return mRunning;
+ }
+
+ /**
+ * This method exists for implementation purpose only and should not be
+ * called directly. Invoke {@link #start()} instead.
+ *
+ * @see #start()
+ */
+ @Override
+ public void run() {
+ nextFrame(false);
+ }
+
+ @Override
+ public void unscheduleSelf(Runnable what) {
+ mRunning = false;
+ super.unscheduleSelf(what);
+ }
+
+ /**
+ * @return The number of frames in the animation
+ */
+ public int getNumberOfFrames() {
+ return mAnimationState.getChildCount();
+ }
+
+ /**
+ * @return The Drawable at the specified frame index
+ */
+ public Drawable getFrame(int index) {
+ return mAnimationState.getChild(index);
+ }
+
+ /**
+ * @return The duration in milliseconds of the frame at the
+ * specified index
+ */
+ public int getDuration(int i) {
+ return mAnimationState.mDurations[i];
+ }
+
+ /**
+ * @return True of the animation will play once, false otherwise
+ */
+ public boolean isOneShot() {
+ return mAnimationState.mOneShot;
+ }
+
+ /**
+ * Sets whether the animation should play once or repeat.
+ *
+ * @param oneShot Pass true if the animation should only play once
+ */
+ public void setOneShot(boolean oneShot) {
+ mAnimationState.mOneShot = oneShot;
+ }
+
+ /**
+ * Adds a frame to the animation
+ *
+ * @param frame The frame to add
+ * @param duration How long in milliseconds the frame should appear
+ */
+ public void addFrame(@NonNull Drawable frame, int duration) {
+ mAnimationState.addFrame(frame, duration);
+ if (!mRunning) {
+ setFrame(0, true, false);
+ }
+ }
+
+ private void nextFrame(boolean unschedule) {
+ int nextFrame = mCurFrame + 1;
+ final int numFrames = mAnimationState.getChildCount();
+ final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1);
+
+ // Loop if necessary. One-shot animations should never hit this case.
+ if (!mAnimationState.mOneShot && nextFrame >= numFrames) {
+ nextFrame = 0;
+ }
+
+ setFrame(nextFrame, unschedule, !isLastFrame);
+ }
+
+ private void setFrame(int frame, boolean unschedule, boolean animate) {
+ if (frame >= mAnimationState.getChildCount()) {
+ return;
+ }
+ mAnimating = animate;
+ mCurFrame = frame;
+ selectDrawable(frame);
+ if (unschedule || animate) {
+ unscheduleSelf(this);
+ }
+ if (animate) {
+ // Unscheduling may have clobbered these values; restore them
+ mCurFrame = frame;
+ mRunning = true;
+ scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
+ }
+ }
+
+ @Override
+ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawable);
+ super.inflateWithAttributes(r, parser, a, R.styleable.AnimationDrawable_visible);
+ updateStateFromTypedArray(a);
+ updateDensity(r);
+ a.recycle();
+
+ inflateChildElements(r, parser, attrs, theme);
+
+ setFrame(0, true, false);
+ }
+
+ private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
+ Theme theme) throws XmlPullParserException, IOException {
+ int type;
+
+ final int innerDepth = parser.getDepth()+1;
+ int depth;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (depth > innerDepth || !parser.getName().equals("item")) {
+ continue;
+ }
+
+ final TypedArray a = obtainAttributes(r, theme, attrs,
+ R.styleable.AnimationDrawableItem);
+
+ final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1);
+ if (duration < 0) {
+ throw new XmlPullParserException(parser.getPositionDescription()
+ + ": <item> tag requires a 'duration' attribute");
+ }
+
+ Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable);
+
+ a.recycle();
+
+ if (dr == null) {
+ while ((type=parser.next()) == XmlPullParser.TEXT) {
+ // Empty
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException(parser.getPositionDescription()
+ + ": <item> tag requires a 'drawable' attribute or child tag"
+ + " defining a drawable");
+ }
+ dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
+ }
+
+ mAnimationState.addFrame(dr, duration);
+ if (dr != null) {
+ dr.setCallback(this);
+ }
+ }
+ }
+
+ private void updateStateFromTypedArray(TypedArray a) {
+ mAnimationState.mVariablePadding = a.getBoolean(
+ R.styleable.AnimationDrawable_variablePadding, mAnimationState.mVariablePadding);
+
+ mAnimationState.mOneShot = a.getBoolean(
+ R.styleable.AnimationDrawable_oneshot, mAnimationState.mOneShot);
+ }
+
+ @Override
+ @NonNull
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mAnimationState.mutate();
+ mMutated = true;
+ }
+ return this;
+ }
+
+ @Override
+ AnimationState cloneConstantState() {
+ return new AnimationState(mAnimationState, this, null);
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mMutated = false;
+ }
+
+ private final static class AnimationState extends DrawableContainerState {
+ private int[] mDurations;
+ private boolean mOneShot = false;
+
+ AnimationState(AnimationState orig, AnimationDrawable owner, Resources res) {
+ super(orig, owner, res);
+
+ if (orig != null) {
+ mDurations = orig.mDurations;
+ mOneShot = orig.mOneShot;
+ } else {
+ mDurations = new int[getCapacity()];
+ mOneShot = false;
+ }
+ }
+
+ private void mutate() {
+ mDurations = mDurations.clone();
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new AnimationDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new AnimationDrawable(this, res);
+ }
+
+ public void addFrame(Drawable dr, int dur) {
+ // Do not combine the following. The array index must be evaluated before
+ // the array is accessed because super.addChild(dr) has a side effect on mDurations.
+ int pos = super.addChild(dr);
+ mDurations[pos] = dur;
+ }
+
+ @Override
+ public void growArray(int oldSize, int newSize) {
+ super.growArray(oldSize, newSize);
+ int[] newDurations = new int[newSize];
+ System.arraycopy(mDurations, 0, newDurations, 0, oldSize);
+ mDurations = newDurations;
+ }
+ }
+
+ @Override
+ protected void setConstantState(@NonNull DrawableContainerState state) {
+ super.setConstantState(state);
+
+ if (state instanceof AnimationState) {
+ mAnimationState = (AnimationState) state;
+ }
+ }
+
+ private AnimationDrawable(AnimationState state, Resources res) {
+ final AnimationState as = new AnimationState(state, this, res);
+ setConstantState(as);
+ if (state != null) {
+ setFrame(0, true, false);
+ }
+ }
+}
+
diff --git a/android/graphics/drawable/BitmapDrawable.java b/android/graphics/drawable/BitmapDrawable.java
new file mode 100644
index 0000000..e3740e3
--- /dev/null
+++ b/android/graphics/drawable/BitmapDrawable.java
@@ -0,0 +1,1036 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import android.annotation.NonNull;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.Xfermode;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.LayoutDirection;
+import android.util.TypedValue;
+import android.view.Gravity;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A Drawable that wraps a bitmap and can be tiled, stretched, or aligned. You can create a
+ * BitmapDrawable from a file path, an input stream, through XML inflation, or from
+ * a {@link android.graphics.Bitmap} object.
+ * <p>It can be defined in an XML file with the <code><bitmap></code> element. For more
+ * information, see the guide to <a
+ * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
+ * <p>
+ * Also see the {@link android.graphics.Bitmap} class, which handles the management and
+ * transformation of raw bitmap graphics, and should be used when drawing to a
+ * {@link android.graphics.Canvas}.
+ * </p>
+ *
+ * @attr ref android.R.styleable#BitmapDrawable_src
+ * @attr ref android.R.styleable#BitmapDrawable_antialias
+ * @attr ref android.R.styleable#BitmapDrawable_filter
+ * @attr ref android.R.styleable#BitmapDrawable_dither
+ * @attr ref android.R.styleable#BitmapDrawable_gravity
+ * @attr ref android.R.styleable#BitmapDrawable_mipMap
+ * @attr ref android.R.styleable#BitmapDrawable_tileMode
+ */
+public class BitmapDrawable extends Drawable {
+ private static final int DEFAULT_PAINT_FLAGS =
+ Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG;
+
+ // Constants for {@link android.R.styleable#BitmapDrawable_tileMode}.
+ private static final int TILE_MODE_UNDEFINED = -2;
+ private static final int TILE_MODE_DISABLED = -1;
+ private static final int TILE_MODE_CLAMP = 0;
+ private static final int TILE_MODE_REPEAT = 1;
+ private static final int TILE_MODE_MIRROR = 2;
+
+ private final Rect mDstRect = new Rect(); // #updateDstRectAndInsetsIfDirty() sets this
+
+ private BitmapState mBitmapState;
+ private PorterDuffColorFilter mTintFilter;
+
+ private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
+
+ private boolean mDstRectAndInsetsDirty = true;
+ private boolean mMutated;
+
+ // These are scaled to match the target density.
+ private int mBitmapWidth;
+ private int mBitmapHeight;
+
+ /** Optical insets due to gravity. */
+ private Insets mOpticalInsets = Insets.NONE;
+
+ // Mirroring matrix for using with Shaders
+ private Matrix mMirrorMatrix;
+
+ /**
+ * Create an empty drawable, not dealing with density.
+ * @deprecated Use {@link #BitmapDrawable(android.content.res.Resources, android.graphics.Bitmap)}
+ * instead to specify a bitmap to draw with and ensure the correct density is set.
+ */
+ @Deprecated
+ public BitmapDrawable() {
+ mBitmapState = new BitmapState((Bitmap) null);
+ }
+
+ /**
+ * Create an empty drawable, setting initial target density based on
+ * the display metrics of the resources.
+ *
+ * @deprecated Use {@link #BitmapDrawable(android.content.res.Resources, android.graphics.Bitmap)}
+ * instead to specify a bitmap to draw with.
+ */
+ @SuppressWarnings("unused")
+ @Deprecated
+ public BitmapDrawable(Resources res) {
+ mBitmapState = new BitmapState((Bitmap) null);
+ mBitmapState.mTargetDensity = mTargetDensity;
+ }
+
+ /**
+ * Create drawable from a bitmap, not dealing with density.
+ * @deprecated Use {@link #BitmapDrawable(Resources, Bitmap)} to ensure
+ * that the drawable has correctly set its target density.
+ */
+ @Deprecated
+ public BitmapDrawable(Bitmap bitmap) {
+ this(new BitmapState(bitmap), null);
+ }
+
+ /**
+ * Create drawable from a bitmap, setting initial target density based on
+ * the display metrics of the resources.
+ */
+ public BitmapDrawable(Resources res, Bitmap bitmap) {
+ this(new BitmapState(bitmap), res);
+ mBitmapState.mTargetDensity = mTargetDensity;
+ }
+
+ /**
+ * Create a drawable by opening a given file path and decoding the bitmap.
+ * @deprecated Use {@link #BitmapDrawable(Resources, String)} to ensure
+ * that the drawable has correctly set its target density.
+ */
+ @Deprecated
+ public BitmapDrawable(String filepath) {
+ this(new BitmapState(BitmapFactory.decodeFile(filepath)), null);
+ if (mBitmapState.mBitmap == null) {
+ android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath);
+ }
+ }
+
+ /**
+ * Create a drawable by opening a given file path and decoding the bitmap.
+ */
+ @SuppressWarnings("unused")
+ public BitmapDrawable(Resources res, String filepath) {
+ this(new BitmapState(BitmapFactory.decodeFile(filepath)), null);
+ mBitmapState.mTargetDensity = mTargetDensity;
+ if (mBitmapState.mBitmap == null) {
+ android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath);
+ }
+ }
+
+ /**
+ * Create a drawable by decoding a bitmap from the given input stream.
+ * @deprecated Use {@link #BitmapDrawable(Resources, java.io.InputStream)} to ensure
+ * that the drawable has correctly set its target density.
+ */
+ @Deprecated
+ public BitmapDrawable(java.io.InputStream is) {
+ this(new BitmapState(BitmapFactory.decodeStream(is)), null);
+ if (mBitmapState.mBitmap == null) {
+ android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is);
+ }
+ }
+
+ /**
+ * Create a drawable by decoding a bitmap from the given input stream.
+ */
+ @SuppressWarnings("unused")
+ public BitmapDrawable(Resources res, java.io.InputStream is) {
+ this(new BitmapState(BitmapFactory.decodeStream(is)), null);
+ mBitmapState.mTargetDensity = mTargetDensity;
+ if (mBitmapState.mBitmap == null) {
+ android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is);
+ }
+ }
+
+ /**
+ * Returns the paint used to render this drawable.
+ */
+ public final Paint getPaint() {
+ return mBitmapState.mPaint;
+ }
+
+ /**
+ * Returns the bitmap used by this drawable to render. May be null.
+ */
+ public final Bitmap getBitmap() {
+ return mBitmapState.mBitmap;
+ }
+
+ private void computeBitmapSize() {
+ final Bitmap bitmap = mBitmapState.mBitmap;
+ if (bitmap != null) {
+ mBitmapWidth = bitmap.getScaledWidth(mTargetDensity);
+ mBitmapHeight = bitmap.getScaledHeight(mTargetDensity);
+ } else {
+ mBitmapWidth = mBitmapHeight = -1;
+ }
+ }
+
+ /** @hide */
+ public void setBitmap(Bitmap bitmap) {
+ if (mBitmapState.mBitmap != bitmap) {
+ mBitmapState.mBitmap = bitmap;
+ computeBitmapSize();
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Set the density scale at which this drawable will be rendered. This
+ * method assumes the drawable will be rendered at the same density as the
+ * specified canvas.
+ *
+ * @param canvas The Canvas from which the density scale must be obtained.
+ *
+ * @see android.graphics.Bitmap#setDensity(int)
+ * @see android.graphics.Bitmap#getDensity()
+ */
+ public void setTargetDensity(Canvas canvas) {
+ setTargetDensity(canvas.getDensity());
+ }
+
+ /**
+ * Set the density scale at which this drawable will be rendered.
+ *
+ * @param metrics The DisplayMetrics indicating the density scale for this drawable.
+ *
+ * @see android.graphics.Bitmap#setDensity(int)
+ * @see android.graphics.Bitmap#getDensity()
+ */
+ public void setTargetDensity(DisplayMetrics metrics) {
+ setTargetDensity(metrics.densityDpi);
+ }
+
+ /**
+ * Set the density at which this drawable will be rendered.
+ *
+ * @param density The density scale for this drawable.
+ *
+ * @see android.graphics.Bitmap#setDensity(int)
+ * @see android.graphics.Bitmap#getDensity()
+ */
+ public void setTargetDensity(int density) {
+ if (mTargetDensity != density) {
+ mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
+ if (mBitmapState.mBitmap != null) {
+ computeBitmapSize();
+ }
+ invalidateSelf();
+ }
+ }
+
+ /** Get the gravity used to position/stretch the bitmap within its bounds.
+ * See android.view.Gravity
+ * @return the gravity applied to the bitmap
+ */
+ public int getGravity() {
+ return mBitmapState.mGravity;
+ }
+
+ /** Set the gravity used to position/stretch the bitmap within its bounds.
+ See android.view.Gravity
+ * @param gravity the gravity
+ */
+ public void setGravity(int gravity) {
+ if (mBitmapState.mGravity != gravity) {
+ mBitmapState.mGravity = gravity;
+ mDstRectAndInsetsDirty = true;
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Enables or disables the mipmap hint for this drawable's bitmap.
+ * See {@link Bitmap#setHasMipMap(boolean)} for more information.
+ *
+ * If the bitmap is null calling this method has no effect.
+ *
+ * @param mipMap True if the bitmap should use mipmaps, false otherwise.
+ *
+ * @see #hasMipMap()
+ */
+ public void setMipMap(boolean mipMap) {
+ if (mBitmapState.mBitmap != null) {
+ mBitmapState.mBitmap.setHasMipMap(mipMap);
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Indicates whether the mipmap hint is enabled on this drawable's bitmap.
+ *
+ * @return True if the mipmap hint is set, false otherwise. If the bitmap
+ * is null, this method always returns false.
+ *
+ * @see #setMipMap(boolean)
+ * @attr ref android.R.styleable#BitmapDrawable_mipMap
+ */
+ public boolean hasMipMap() {
+ return mBitmapState.mBitmap != null && mBitmapState.mBitmap.hasMipMap();
+ }
+
+ /**
+ * Enables or disables anti-aliasing for this drawable. Anti-aliasing affects
+ * the edges of the bitmap only so it applies only when the drawable is rotated.
+ *
+ * @param aa True if the bitmap should be anti-aliased, false otherwise.
+ *
+ * @see #hasAntiAlias()
+ */
+ public void setAntiAlias(boolean aa) {
+ mBitmapState.mPaint.setAntiAlias(aa);
+ invalidateSelf();
+ }
+
+ /**
+ * Indicates whether anti-aliasing is enabled for this drawable.
+ *
+ * @return True if anti-aliasing is enabled, false otherwise.
+ *
+ * @see #setAntiAlias(boolean)
+ */
+ public boolean hasAntiAlias() {
+ return mBitmapState.mPaint.isAntiAlias();
+ }
+
+ @Override
+ public void setFilterBitmap(boolean filter) {
+ mBitmapState.mPaint.setFilterBitmap(filter);
+ invalidateSelf();
+ }
+
+ @Override
+ public boolean isFilterBitmap() {
+ return mBitmapState.mPaint.isFilterBitmap();
+ }
+
+ @Override
+ public void setDither(boolean dither) {
+ mBitmapState.mPaint.setDither(dither);
+ invalidateSelf();
+ }
+
+ /**
+ * Indicates the repeat behavior of this drawable on the X axis.
+ *
+ * @return {@link android.graphics.Shader.TileMode#CLAMP} if the bitmap does not repeat,
+ * {@link android.graphics.Shader.TileMode#REPEAT} or
+ * {@link android.graphics.Shader.TileMode#MIRROR} otherwise.
+ */
+ public Shader.TileMode getTileModeX() {
+ return mBitmapState.mTileModeX;
+ }
+
+ /**
+ * Indicates the repeat behavior of this drawable on the Y axis.
+ *
+ * @return {@link android.graphics.Shader.TileMode#CLAMP} if the bitmap does not repeat,
+ * {@link android.graphics.Shader.TileMode#REPEAT} or
+ * {@link android.graphics.Shader.TileMode#MIRROR} otherwise.
+ */
+ public Shader.TileMode getTileModeY() {
+ return mBitmapState.mTileModeY;
+ }
+
+ /**
+ * Sets the repeat behavior of this drawable on the X axis. By default, the drawable
+ * does not repeat its bitmap. Using {@link android.graphics.Shader.TileMode#REPEAT} or
+ * {@link android.graphics.Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled)
+ * if the bitmap is smaller than this drawable.
+ *
+ * @param mode The repeat mode for this drawable.
+ *
+ * @see #setTileModeY(android.graphics.Shader.TileMode)
+ * @see #setTileModeXY(android.graphics.Shader.TileMode, android.graphics.Shader.TileMode)
+ * @attr ref android.R.styleable#BitmapDrawable_tileModeX
+ */
+ public void setTileModeX(Shader.TileMode mode) {
+ setTileModeXY(mode, mBitmapState.mTileModeY);
+ }
+
+ /**
+ * Sets the repeat behavior of this drawable on the Y axis. By default, the drawable
+ * does not repeat its bitmap. Using {@link android.graphics.Shader.TileMode#REPEAT} or
+ * {@link android.graphics.Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled)
+ * if the bitmap is smaller than this drawable.
+ *
+ * @param mode The repeat mode for this drawable.
+ *
+ * @see #setTileModeX(android.graphics.Shader.TileMode)
+ * @see #setTileModeXY(android.graphics.Shader.TileMode, android.graphics.Shader.TileMode)
+ * @attr ref android.R.styleable#BitmapDrawable_tileModeY
+ */
+ public final void setTileModeY(Shader.TileMode mode) {
+ setTileModeXY(mBitmapState.mTileModeX, mode);
+ }
+
+ /**
+ * Sets the repeat behavior of this drawable on both axis. By default, the drawable
+ * does not repeat its bitmap. Using {@link android.graphics.Shader.TileMode#REPEAT} or
+ * {@link android.graphics.Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled)
+ * if the bitmap is smaller than this drawable.
+ *
+ * @param xmode The X repeat mode for this drawable.
+ * @param ymode The Y repeat mode for this drawable.
+ *
+ * @see #setTileModeX(android.graphics.Shader.TileMode)
+ * @see #setTileModeY(android.graphics.Shader.TileMode)
+ */
+ public void setTileModeXY(Shader.TileMode xmode, Shader.TileMode ymode) {
+ final BitmapState state = mBitmapState;
+ if (state.mTileModeX != xmode || state.mTileModeY != ymode) {
+ state.mTileModeX = xmode;
+ state.mTileModeY = ymode;
+ state.mRebuildShader = true;
+ mDstRectAndInsetsDirty = true;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setAutoMirrored(boolean mirrored) {
+ if (mBitmapState.mAutoMirrored != mirrored) {
+ mBitmapState.mAutoMirrored = mirrored;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public final boolean isAutoMirrored() {
+ return mBitmapState.mAutoMirrored;
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations() | mBitmapState.getChangingConfigurations();
+ }
+
+ private boolean needMirroring() {
+ return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ mDstRectAndInsetsDirty = true;
+
+ final Bitmap bitmap = mBitmapState.mBitmap;
+ final Shader shader = mBitmapState.mPaint.getShader();
+ if (bitmap != null && shader != null) {
+ updateShaderMatrix(bitmap, mBitmapState.mPaint, shader, needMirroring());
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Bitmap bitmap = mBitmapState.mBitmap;
+ if (bitmap == null) {
+ return;
+ }
+
+ final BitmapState state = mBitmapState;
+ final Paint paint = state.mPaint;
+ if (state.mRebuildShader) {
+ final Shader.TileMode tmx = state.mTileModeX;
+ final Shader.TileMode tmy = state.mTileModeY;
+ if (tmx == null && tmy == null) {
+ paint.setShader(null);
+ } else {
+ paint.setShader(new BitmapShader(bitmap,
+ tmx == null ? Shader.TileMode.CLAMP : tmx,
+ tmy == null ? Shader.TileMode.CLAMP : tmy));
+ }
+
+ state.mRebuildShader = false;
+ }
+
+ final int restoreAlpha;
+ if (state.mBaseAlpha != 1.0f) {
+ final Paint p = getPaint();
+ restoreAlpha = p.getAlpha();
+ p.setAlpha((int) (restoreAlpha * state.mBaseAlpha + 0.5f));
+ } else {
+ restoreAlpha = -1;
+ }
+
+ final boolean clearColorFilter;
+ if (mTintFilter != null && paint.getColorFilter() == null) {
+ paint.setColorFilter(mTintFilter);
+ clearColorFilter = true;
+ } else {
+ clearColorFilter = false;
+ }
+
+ updateDstRectAndInsetsIfDirty();
+ final Shader shader = paint.getShader();
+ final boolean needMirroring = needMirroring();
+ if (shader == null) {
+ if (needMirroring) {
+ canvas.save();
+ // Mirror the bitmap
+ canvas.translate(mDstRect.right - mDstRect.left, 0);
+ canvas.scale(-1.0f, 1.0f);
+ }
+
+ canvas.drawBitmap(bitmap, null, mDstRect, paint);
+
+ if (needMirroring) {
+ canvas.restore();
+ }
+ } else {
+ updateShaderMatrix(bitmap, paint, shader, needMirroring);
+ canvas.drawRect(mDstRect, paint);
+ }
+
+ if (clearColorFilter) {
+ paint.setColorFilter(null);
+ }
+
+ if (restoreAlpha >= 0) {
+ paint.setAlpha(restoreAlpha);
+ }
+ }
+
+ /**
+ * Updates the {@code paint}'s shader matrix to be consistent with the
+ * destination size and layout direction.
+ *
+ * @param bitmap the bitmap to be drawn
+ * @param paint the paint used to draw the bitmap
+ * @param shader the shader to set on the paint
+ * @param needMirroring whether the bitmap should be mirrored
+ */
+ private void updateShaderMatrix(@NonNull Bitmap bitmap, @NonNull Paint paint,
+ @NonNull Shader shader, boolean needMirroring) {
+ final int sourceDensity = bitmap.getDensity();
+ final int targetDensity = mTargetDensity;
+ final boolean needScaling = sourceDensity != 0 && sourceDensity != targetDensity;
+ if (needScaling || needMirroring) {
+ final Matrix matrix = getOrCreateMirrorMatrix();
+ matrix.reset();
+
+ if (needMirroring) {
+ final int dx = mDstRect.right - mDstRect.left;
+ matrix.setTranslate(dx, 0);
+ matrix.setScale(-1, 1);
+ }
+
+ if (needScaling) {
+ final float densityScale = targetDensity / (float) sourceDensity;
+ matrix.postScale(densityScale, densityScale);
+ }
+
+ shader.setLocalMatrix(matrix);
+ } else {
+ mMirrorMatrix = null;
+ shader.setLocalMatrix(Matrix.IDENTITY_MATRIX);
+ }
+
+ paint.setShader(shader);
+ }
+
+ private Matrix getOrCreateMirrorMatrix() {
+ if (mMirrorMatrix == null) {
+ mMirrorMatrix = new Matrix();
+ }
+ return mMirrorMatrix;
+ }
+
+ private void updateDstRectAndInsetsIfDirty() {
+ if (mDstRectAndInsetsDirty) {
+ if (mBitmapState.mTileModeX == null && mBitmapState.mTileModeY == null) {
+ final Rect bounds = getBounds();
+ final int layoutDirection = getLayoutDirection();
+ Gravity.apply(mBitmapState.mGravity, mBitmapWidth, mBitmapHeight,
+ bounds, mDstRect, layoutDirection);
+
+ final int left = mDstRect.left - bounds.left;
+ final int top = mDstRect.top - bounds.top;
+ final int right = bounds.right - mDstRect.right;
+ final int bottom = bounds.bottom - mDstRect.bottom;
+ mOpticalInsets = Insets.of(left, top, right, bottom);
+ } else {
+ copyBounds(mDstRect);
+ mOpticalInsets = Insets.NONE;
+ }
+ }
+ mDstRectAndInsetsDirty = false;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public Insets getOpticalInsets() {
+ updateDstRectAndInsetsIfDirty();
+ return mOpticalInsets;
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ updateDstRectAndInsetsIfDirty();
+ outline.setRect(mDstRect);
+
+ // Only opaque Bitmaps can report a non-0 alpha,
+ // since only they are guaranteed to fill their bounds
+ boolean opaqueOverShape = mBitmapState.mBitmap != null
+ && !mBitmapState.mBitmap.hasAlpha();
+ outline.setAlpha(opaqueOverShape ? getAlpha() / 255.0f : 0.0f);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ final int oldAlpha = mBitmapState.mPaint.getAlpha();
+ if (alpha != oldAlpha) {
+ mBitmapState.mPaint.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public int getAlpha() {
+ return mBitmapState.mPaint.getAlpha();
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mBitmapState.mPaint.setColorFilter(colorFilter);
+ invalidateSelf();
+ }
+
+ @Override
+ public ColorFilter getColorFilter() {
+ return mBitmapState.mPaint.getColorFilter();
+ }
+
+ @Override
+ public void setTintList(ColorStateList tint) {
+ final BitmapState state = mBitmapState;
+ if (state.mTint != tint) {
+ state.mTint = tint;
+ mTintFilter = updateTintFilter(mTintFilter, tint, mBitmapState.mTintMode);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setTintMode(PorterDuff.Mode tintMode) {
+ final BitmapState state = mBitmapState;
+ if (state.mTintMode != tintMode) {
+ state.mTintMode = tintMode;
+ mTintFilter = updateTintFilter(mTintFilter, mBitmapState.mTint, tintMode);
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * @hide only needed by a hack within ProgressBar
+ */
+ public ColorStateList getTint() {
+ return mBitmapState.mTint;
+ }
+
+ /**
+ * @hide only needed by a hack within ProgressBar
+ */
+ public Mode getTintMode() {
+ return mBitmapState.mTintMode;
+ }
+
+ /**
+ * @hide Candidate for future API inclusion
+ */
+ @Override
+ public void setXfermode(Xfermode xfermode) {
+ mBitmapState.mPaint.setXfermode(xfermode);
+ invalidateSelf();
+ }
+
+ /**
+ * A mutable BitmapDrawable still shares its Bitmap with any other Drawable
+ * that comes from the same resource.
+ *
+ * @return This drawable.
+ */
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mBitmapState = new BitmapState(mBitmapState);
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mMutated = false;
+ }
+
+ @Override
+ protected boolean onStateChange(int[] stateSet) {
+ final BitmapState state = mBitmapState;
+ if (state.mTint != null && state.mTintMode != null) {
+ mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isStateful() {
+ return (mBitmapState.mTint != null && mBitmapState.mTint.isStateful())
+ || super.isStateful();
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return mBitmapState.mTint != null && mBitmapState.mTint.hasFocusStateSpecified();
+ }
+
+ @Override
+ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
+ throws XmlPullParserException, IOException {
+ super.inflate(r, parser, attrs, theme);
+
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.BitmapDrawable);
+ updateStateFromTypedArray(a, mSrcDensityOverride);
+ verifyRequiredAttributes(a);
+ a.recycle();
+
+ // Update local properties.
+ updateLocalState(r);
+ }
+
+ /**
+ * Ensures all required attributes are set.
+ *
+ * @throws XmlPullParserException if any required attributes are missing
+ */
+ private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
+ // If we're not waiting on a theme, verify required attributes.
+ final BitmapState state = mBitmapState;
+ if (state.mBitmap == null && (state.mThemeAttrs == null
+ || state.mThemeAttrs[R.styleable.BitmapDrawable_src] == 0)) {
+ throw new XmlPullParserException(a.getPositionDescription() +
+ ": <bitmap> requires a valid 'src' attribute");
+ }
+ }
+
+ /**
+ * Updates the constant state from the values in the typed array.
+ */
+ private void updateStateFromTypedArray(TypedArray a, int srcDensityOverride)
+ throws XmlPullParserException {
+ final Resources r = a.getResources();
+ final BitmapState state = mBitmapState;
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ state.mSrcDensityOverride = srcDensityOverride;
+
+ state.mTargetDensity = Drawable.resolveDensity(r, 0);
+
+ final int srcResId = a.getResourceId(R.styleable.BitmapDrawable_src, 0);
+ if (srcResId != 0) {
+ final TypedValue value = new TypedValue();
+ r.getValueForDensity(srcResId, srcDensityOverride, value, true);
+
+ // Pretend the requested density is actually the display density. If
+ // the drawable returned is not the requested density, then force it
+ // to be scaled later by dividing its density by the ratio of
+ // requested density to actual device density. Drawables that have
+ // undefined density or no density don't need to be handled here.
+ if (srcDensityOverride > 0 && value.density > 0
+ && value.density != TypedValue.DENSITY_NONE) {
+ if (value.density == srcDensityOverride) {
+ value.density = r.getDisplayMetrics().densityDpi;
+ } else {
+ value.density =
+ (value.density * r.getDisplayMetrics().densityDpi) / srcDensityOverride;
+ }
+ }
+
+ Bitmap bitmap = null;
+ try (InputStream is = r.openRawResource(srcResId, value)) {
+ bitmap = BitmapFactory.decodeResourceStream(r, value, is, null, null);
+ } catch (Exception e) {
+ // Do nothing and pick up the error below.
+ }
+
+ if (bitmap == null) {
+ throw new XmlPullParserException(a.getPositionDescription() +
+ ": <bitmap> requires a valid 'src' attribute");
+ }
+
+ state.mBitmap = bitmap;
+ }
+
+ final boolean defMipMap = state.mBitmap != null ? state.mBitmap.hasMipMap() : false;
+ setMipMap(a.getBoolean(R.styleable.BitmapDrawable_mipMap, defMipMap));
+
+ state.mAutoMirrored = a.getBoolean(
+ R.styleable.BitmapDrawable_autoMirrored, state.mAutoMirrored);
+ state.mBaseAlpha = a.getFloat(R.styleable.BitmapDrawable_alpha, state.mBaseAlpha);
+
+ final int tintMode = a.getInt(R.styleable.BitmapDrawable_tintMode, -1);
+ if (tintMode != -1) {
+ state.mTintMode = Drawable.parseTintMode(tintMode, Mode.SRC_IN);
+ }
+
+ final ColorStateList tint = a.getColorStateList(R.styleable.BitmapDrawable_tint);
+ if (tint != null) {
+ state.mTint = tint;
+ }
+
+ final Paint paint = mBitmapState.mPaint;
+ paint.setAntiAlias(a.getBoolean(
+ R.styleable.BitmapDrawable_antialias, paint.isAntiAlias()));
+ paint.setFilterBitmap(a.getBoolean(
+ R.styleable.BitmapDrawable_filter, paint.isFilterBitmap()));
+ paint.setDither(a.getBoolean(R.styleable.BitmapDrawable_dither, paint.isDither()));
+
+ setGravity(a.getInt(R.styleable.BitmapDrawable_gravity, state.mGravity));
+
+ final int tileMode = a.getInt(R.styleable.BitmapDrawable_tileMode, TILE_MODE_UNDEFINED);
+ if (tileMode != TILE_MODE_UNDEFINED) {
+ final Shader.TileMode mode = parseTileMode(tileMode);
+ setTileModeXY(mode, mode);
+ }
+
+ final int tileModeX = a.getInt(R.styleable.BitmapDrawable_tileModeX, TILE_MODE_UNDEFINED);
+ if (tileModeX != TILE_MODE_UNDEFINED) {
+ setTileModeX(parseTileMode(tileModeX));
+ }
+
+ final int tileModeY = a.getInt(R.styleable.BitmapDrawable_tileModeY, TILE_MODE_UNDEFINED);
+ if (tileModeY != TILE_MODE_UNDEFINED) {
+ setTileModeY(parseTileMode(tileModeY));
+ }
+ }
+
+ @Override
+ public void applyTheme(Theme t) {
+ super.applyTheme(t);
+
+ final BitmapState state = mBitmapState;
+ if (state == null) {
+ return;
+ }
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.BitmapDrawable);
+ try {
+ updateStateFromTypedArray(a, state.mSrcDensityOverride);
+ } catch (XmlPullParserException e) {
+ rethrowAsRuntimeException(e);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ // Apply theme to contained color state list.
+ if (state.mTint != null && state.mTint.canApplyTheme()) {
+ state.mTint = state.mTint.obtainForTheme(t);
+ }
+
+ // Update local properties.
+ updateLocalState(t.getResources());
+ }
+
+ private static Shader.TileMode parseTileMode(int tileMode) {
+ switch (tileMode) {
+ case TILE_MODE_CLAMP:
+ return Shader.TileMode.CLAMP;
+ case TILE_MODE_REPEAT:
+ return Shader.TileMode.REPEAT;
+ case TILE_MODE_MIRROR:
+ return Shader.TileMode.MIRROR;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mBitmapState != null && mBitmapState.canApplyTheme();
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mBitmapWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mBitmapHeight;
+ }
+
+ @Override
+ public int getOpacity() {
+ if (mBitmapState.mGravity != Gravity.FILL) {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ final Bitmap bitmap = mBitmapState.mBitmap;
+ return (bitmap == null || bitmap.hasAlpha() || mBitmapState.mPaint.getAlpha() < 255) ?
+ PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
+ }
+
+ @Override
+ public final ConstantState getConstantState() {
+ mBitmapState.mChangingConfigurations |= getChangingConfigurations();
+ return mBitmapState;
+ }
+
+ final static class BitmapState extends ConstantState {
+ final Paint mPaint;
+
+ // Values loaded during inflation.
+ int[] mThemeAttrs = null;
+ Bitmap mBitmap = null;
+ ColorStateList mTint = null;
+ Mode mTintMode = DEFAULT_TINT_MODE;
+ int mGravity = Gravity.FILL;
+ float mBaseAlpha = 1.0f;
+ Shader.TileMode mTileModeX = null;
+ Shader.TileMode mTileModeY = null;
+
+ // The density to use when looking up the bitmap in Resources. A value of 0 means use
+ // the system's density.
+ int mSrcDensityOverride = 0;
+
+ // The density at which to render the bitmap.
+ int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
+
+ boolean mAutoMirrored = false;
+
+ @Config int mChangingConfigurations;
+ boolean mRebuildShader;
+
+ BitmapState(Bitmap bitmap) {
+ mBitmap = bitmap;
+ mPaint = new Paint(DEFAULT_PAINT_FLAGS);
+ }
+
+ BitmapState(BitmapState bitmapState) {
+ mBitmap = bitmapState.mBitmap;
+ mTint = bitmapState.mTint;
+ mTintMode = bitmapState.mTintMode;
+ mThemeAttrs = bitmapState.mThemeAttrs;
+ mChangingConfigurations = bitmapState.mChangingConfigurations;
+ mGravity = bitmapState.mGravity;
+ mTileModeX = bitmapState.mTileModeX;
+ mTileModeY = bitmapState.mTileModeY;
+ mSrcDensityOverride = bitmapState.mSrcDensityOverride;
+ mTargetDensity = bitmapState.mTargetDensity;
+ mBaseAlpha = bitmapState.mBaseAlpha;
+ mPaint = new Paint(bitmapState.mPaint);
+ mRebuildShader = bitmapState.mRebuildShader;
+ mAutoMirrored = bitmapState.mAutoMirrored;
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mThemeAttrs != null || mTint != null && mTint.canApplyTheme();
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new BitmapDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new BitmapDrawable(this, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations
+ | (mTint != null ? mTint.getChangingConfigurations() : 0);
+ }
+ }
+
+ /**
+ * The one constructor to rule them all. This is called by all public
+ * constructors to set the state and initialize local properties.
+ */
+ private BitmapDrawable(BitmapState state, Resources res) {
+ mBitmapState = state;
+
+ updateLocalState(res);
+ }
+
+ /**
+ * Initializes local dynamic properties from state. This should be called
+ * after significant state changes, e.g. from the One True Constructor and
+ * after inflating or applying a theme.
+ */
+ private void updateLocalState(Resources res) {
+ mTargetDensity = resolveDensity(res, mBitmapState.mTargetDensity);
+ mTintFilter = updateTintFilter(mTintFilter, mBitmapState.mTint, mBitmapState.mTintMode);
+ computeBitmapSize();
+ }
+}
diff --git a/android/graphics/drawable/ClipDrawable.java b/android/graphics/drawable/ClipDrawable.java
new file mode 100644
index 0000000..d925b6b
--- /dev/null
+++ b/android/graphics/drawable/ClipDrawable.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.Resources.Theme;
+import android.graphics.*;
+import android.view.Gravity;
+import android.util.AttributeSet;
+
+import java.io.IOException;
+
+/**
+ * A Drawable that clips another Drawable based on this Drawable's current
+ * level value. You can control how much the child Drawable gets clipped in width
+ * and height based on the level, as well as a gravity to control where it is
+ * placed in its overall container. Most often used to implement things like
+ * progress bars, by increasing the drawable's level with {@link
+ * android.graphics.drawable.Drawable#setLevel(int) setLevel()}.
+ * <p class="note"><strong>Note:</strong> The drawable is clipped completely and not visible when
+ * the level is 0 and fully revealed when the level is 10,000.</p>
+ *
+ * <p>It can be defined in an XML file with the <code><clip></code> element. For more
+ * information, see the guide to <a
+ * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
+ *
+ * @attr ref android.R.styleable#ClipDrawable_clipOrientation
+ * @attr ref android.R.styleable#ClipDrawable_gravity
+ * @attr ref android.R.styleable#ClipDrawable_drawable
+ */
+public class ClipDrawable extends DrawableWrapper {
+ public static final int HORIZONTAL = 1;
+ public static final int VERTICAL = 2;
+
+ private static final int MAX_LEVEL = 10000;
+
+ private final Rect mTmpRect = new Rect();
+
+ private ClipState mState;
+
+ ClipDrawable() {
+ this(new ClipState(null, null), null);
+ }
+
+ /**
+ * Creates a new clip drawable with the specified gravity and orientation.
+ *
+ * @param drawable the drawable to clip
+ * @param gravity gravity constant (see {@link Gravity} used to position
+ * the clipped drawable within the parent container
+ * @param orientation bitwise-or of {@link #HORIZONTAL} and/or
+ * {@link #VERTICAL}
+ */
+ public ClipDrawable(Drawable drawable, int gravity, int orientation) {
+ this(new ClipState(null, null), null);
+
+ mState.mGravity = gravity;
+ mState.mOrientation = orientation;
+
+ setDrawable(drawable);
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ClipDrawable);
+
+ // Inflation will advance the XmlPullParser and AttributeSet.
+ super.inflate(r, parser, attrs, theme);
+
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ a.recycle();
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ final ClipState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.ClipDrawable);
+ try {
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ } catch (XmlPullParserException e) {
+ rethrowAsRuntimeException(e);
+ } finally {
+ a.recycle();
+ }
+ }
+ }
+
+ private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
+ // If we're not waiting on a theme, verify required attributes.
+ if (getDrawable() == null && (mState.mThemeAttrs == null
+ || mState.mThemeAttrs[R.styleable.ClipDrawable_drawable] == 0)) {
+ throw new XmlPullParserException(a.getPositionDescription()
+ + ": <clip> tag requires a 'drawable' attribute or "
+ + "child tag defining a drawable");
+ }
+ }
+
+ private void updateStateFromTypedArray(@NonNull TypedArray a) {
+ final ClipState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ state.mOrientation = a.getInt(
+ R.styleable.ClipDrawable_clipOrientation, state.mOrientation);
+ state.mGravity = a.getInt(
+ R.styleable.ClipDrawable_gravity, state.mGravity);
+ }
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ super.onLevelChange(level);
+ invalidateSelf();
+ return true;
+ }
+
+ @Override
+ public int getOpacity() {
+ final Drawable dr = getDrawable();
+ final int opacity = dr.getOpacity();
+ if (opacity == PixelFormat.TRANSPARENT || dr.getLevel() == 0) {
+ return PixelFormat.TRANSPARENT;
+ }
+
+ final int level = getLevel();
+ if (level >= MAX_LEVEL) {
+ return dr.getOpacity();
+ }
+
+ // Some portion of non-transparent drawable is showing.
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Drawable dr = getDrawable();
+ if (dr.getLevel() == 0) {
+ return;
+ }
+
+ final Rect r = mTmpRect;
+ final Rect bounds = getBounds();
+ final int level = getLevel();
+
+ int w = bounds.width();
+ final int iw = 0; //mState.mDrawable.getIntrinsicWidth();
+ if ((mState.mOrientation & HORIZONTAL) != 0) {
+ w -= (w - iw) * (MAX_LEVEL - level) / MAX_LEVEL;
+ }
+
+ int h = bounds.height();
+ final int ih = 0; //mState.mDrawable.getIntrinsicHeight();
+ if ((mState.mOrientation & VERTICAL) != 0) {
+ h -= (h - ih) * (MAX_LEVEL - level) / MAX_LEVEL;
+ }
+
+ final int layoutDirection = getLayoutDirection();
+ Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection);
+
+ if (w > 0 && h > 0) {
+ canvas.save();
+ canvas.clipRect(r);
+ dr.draw(canvas);
+ canvas.restore();
+ }
+ }
+
+ @Override
+ DrawableWrapperState mutateConstantState() {
+ mState = new ClipState(mState, null);
+ return mState;
+ }
+
+ static final class ClipState extends DrawableWrapper.DrawableWrapperState {
+ private int[] mThemeAttrs;
+
+ int mOrientation = HORIZONTAL;
+ int mGravity = Gravity.LEFT;
+
+ ClipState(ClipState orig, Resources res) {
+ super(orig, res);
+
+ if (orig != null) {
+ mOrientation = orig.mOrientation;
+ mGravity = orig.mGravity;
+ }
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new ClipDrawable(this, res);
+ }
+ }
+
+ private ClipDrawable(ClipState state, Resources res) {
+ super(state, res);
+
+ mState = state;
+ }
+}
+
diff --git a/android/graphics/drawable/ColorDrawable.java b/android/graphics/drawable/ColorDrawable.java
new file mode 100644
index 0000000..9ae747d
--- /dev/null
+++ b/android/graphics/drawable/ColorDrawable.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2008 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 android.graphics.drawable;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.pm.ActivityInfo.Config;
+import android.graphics.*;
+import android.graphics.PorterDuff.Mode;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.ViewDebug;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * A specialized Drawable that fills the Canvas with a specified color.
+ * Note that a ColorDrawable ignores the ColorFilter.
+ *
+ * <p>It can be defined in an XML file with the <code><color></code> element.</p>
+ *
+ * @attr ref android.R.styleable#ColorDrawable_color
+ */
+public class ColorDrawable extends Drawable {
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ @ViewDebug.ExportedProperty(deepExport = true, prefix = "state_")
+ private ColorState mColorState;
+ private PorterDuffColorFilter mTintFilter;
+
+ private boolean mMutated;
+
+ /**
+ * Creates a new black ColorDrawable.
+ */
+ public ColorDrawable() {
+ mColorState = new ColorState();
+ }
+
+ /**
+ * Creates a new ColorDrawable with the specified color.
+ *
+ * @param color The color to draw.
+ */
+ public ColorDrawable(@ColorInt int color) {
+ mColorState = new ColorState();
+
+ setColor(color);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations() | mColorState.getChangingConfigurations();
+ }
+
+ /**
+ * A mutable BitmapDrawable still shares its Bitmap with any other Drawable
+ * that comes from the same resource.
+ *
+ * @return This drawable.
+ */
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mColorState = new ColorState(mColorState);
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mMutated = false;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final ColorFilter colorFilter = mPaint.getColorFilter();
+ if ((mColorState.mUseColor >>> 24) != 0 || colorFilter != null || mTintFilter != null) {
+ if (colorFilter == null) {
+ mPaint.setColorFilter(mTintFilter);
+ }
+
+ mPaint.setColor(mColorState.mUseColor);
+ canvas.drawRect(getBounds(), mPaint);
+
+ // Restore original color filter.
+ mPaint.setColorFilter(colorFilter);
+ }
+ }
+
+ /**
+ * Gets the drawable's color value.
+ *
+ * @return int The color to draw.
+ */
+ @ColorInt
+ public int getColor() {
+ return mColorState.mUseColor;
+ }
+
+ /**
+ * Sets the drawable's color value. This action will clobber the results of
+ * prior calls to {@link #setAlpha(int)} on this object, which side-affected
+ * the underlying color.
+ *
+ * @param color The color to draw.
+ */
+ public void setColor(@ColorInt int color) {
+ if (mColorState.mBaseColor != color || mColorState.mUseColor != color) {
+ mColorState.mBaseColor = mColorState.mUseColor = color;
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Returns the alpha value of this drawable's color.
+ *
+ * @return A value between 0 and 255.
+ */
+ @Override
+ public int getAlpha() {
+ return mColorState.mUseColor >>> 24;
+ }
+
+ /**
+ * Sets the color's alpha value.
+ *
+ * @param alpha The alpha value to set, between 0 and 255.
+ */
+ @Override
+ public void setAlpha(int alpha) {
+ alpha += alpha >> 7; // make it 0..256
+ final int baseAlpha = mColorState.mBaseColor >>> 24;
+ final int useAlpha = baseAlpha * alpha >> 8;
+ final int useColor = (mColorState.mBaseColor << 8 >>> 8) | (useAlpha << 24);
+ if (mColorState.mUseColor != useColor) {
+ mColorState.mUseColor = useColor;
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Sets the color filter applied to this color.
+ * <p>
+ * Only supported on version {@link android.os.Build.VERSION_CODES#LOLLIPOP} and
+ * above. Calling this method has no effect on earlier versions.
+ *
+ * @see android.graphics.drawable.Drawable#setColorFilter(ColorFilter)
+ */
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mPaint.setColorFilter(colorFilter);
+ }
+
+ @Override
+ public void setTintList(ColorStateList tint) {
+ mColorState.mTint = tint;
+ mTintFilter = updateTintFilter(mTintFilter, tint, mColorState.mTintMode);
+ invalidateSelf();
+ }
+
+ @Override
+ public void setTintMode(Mode tintMode) {
+ mColorState.mTintMode = tintMode;
+ mTintFilter = updateTintFilter(mTintFilter, mColorState.mTint, tintMode);
+ invalidateSelf();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] stateSet) {
+ final ColorState state = mColorState;
+ if (state.mTint != null && state.mTintMode != null) {
+ mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isStateful() {
+ return mColorState.mTint != null && mColorState.mTint.isStateful();
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return mColorState.mTint != null && mColorState.mTint.hasFocusStateSpecified();
+ }
+
+ /**
+ * @hide
+ * @param mode new transfer mode
+ */
+ @Override
+ public void setXfermode(@Nullable Xfermode mode) {
+ mPaint.setXfermode(mode);
+ invalidateSelf();
+ }
+
+ /**
+ * @hide
+ * @return current transfer mode
+ */
+ @TestApi
+ public Xfermode getXfermode() {
+ return mPaint.getXfermode();
+ }
+
+ @Override
+ public int getOpacity() {
+ if (mTintFilter != null || mPaint.getColorFilter() != null) {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ switch (mColorState.mUseColor >>> 24) {
+ case 255:
+ return PixelFormat.OPAQUE;
+ case 0:
+ return PixelFormat.TRANSPARENT;
+ }
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ outline.setRect(getBounds());
+ outline.setAlpha(getAlpha() / 255.0f);
+ }
+
+ @Override
+ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
+ throws XmlPullParserException, IOException {
+ super.inflate(r, parser, attrs, theme);
+
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ColorDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+
+ updateLocalState(r);
+ }
+
+ /**
+ * Updates the constant state from the values in the typed array.
+ */
+ private void updateStateFromTypedArray(TypedArray a) {
+ final ColorState state = mColorState;
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ state.mBaseColor = a.getColor(R.styleable.ColorDrawable_color, state.mBaseColor);
+ state.mUseColor = state.mBaseColor;
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mColorState.canApplyTheme() || super.canApplyTheme();
+ }
+
+ @Override
+ public void applyTheme(Theme t) {
+ super.applyTheme(t);
+
+ final ColorState state = mColorState;
+ if (state == null) {
+ return;
+ }
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.ColorDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+ }
+
+ if (state.mTint != null && state.mTint.canApplyTheme()) {
+ state.mTint = state.mTint.obtainForTheme(t);
+ }
+
+ updateLocalState(t.getResources());
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ return mColorState;
+ }
+
+ final static class ColorState extends ConstantState {
+ int[] mThemeAttrs;
+ int mBaseColor; // base color, independent of setAlpha()
+ @ViewDebug.ExportedProperty
+ int mUseColor; // basecolor modulated by setAlpha()
+ @Config int mChangingConfigurations;
+ ColorStateList mTint = null;
+ Mode mTintMode = DEFAULT_TINT_MODE;
+
+ ColorState() {
+ // Empty constructor.
+ }
+
+ ColorState(ColorState state) {
+ mThemeAttrs = state.mThemeAttrs;
+ mBaseColor = state.mBaseColor;
+ mUseColor = state.mUseColor;
+ mChangingConfigurations = state.mChangingConfigurations;
+ mTint = state.mTint;
+ mTintMode = state.mTintMode;
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mThemeAttrs != null
+ || (mTint != null && mTint.canApplyTheme());
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new ColorDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new ColorDrawable(this, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations
+ | (mTint != null ? mTint.getChangingConfigurations() : 0);
+ }
+ }
+
+ private ColorDrawable(ColorState state, Resources res) {
+ mColorState = state;
+
+ updateLocalState(res);
+ }
+
+ /**
+ * Initializes local dynamic properties from state. This should be called
+ * after significant state changes, e.g. from the One True Constructor and
+ * after inflating or applying a theme.
+ */
+ private void updateLocalState(Resources r) {
+ mTintFilter = updateTintFilter(mTintFilter, mColorState.mTint, mColorState.mTintMode);
+ }
+}
diff --git a/android/graphics/drawable/Drawable.java b/android/graphics/drawable/Drawable.java
new file mode 100644
index 0000000..f17cd76
--- /dev/null
+++ b/android/graphics/drawable/Drawable.java
@@ -0,0 +1,1594 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.AttrRes;
+import android.annotation.ColorInt;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Insets;
+import android.graphics.NinePatch;
+import android.graphics.Outline;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.Xfermode;
+import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.StateSet;
+import android.util.TypedValue;
+import android.util.Xml;
+import android.view.View;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+
+/**
+ * A Drawable is a general abstraction for "something that can be drawn." Most
+ * often you will deal with Drawable as the type of resource retrieved for
+ * drawing things to the screen; the Drawable class provides a generic API for
+ * dealing with an underlying visual resource that may take a variety of forms.
+ * Unlike a {@link android.view.View}, a Drawable does not have any facility to
+ * receive events or otherwise interact with the user.
+ *
+ * <p>In addition to simple drawing, Drawable provides a number of generic
+ * mechanisms for its client to interact with what is being drawn:
+ *
+ * <ul>
+ * <li> The {@link #setBounds} method <var>must</var> be called to tell the
+ * Drawable where it is drawn and how large it should be. All Drawables
+ * should respect the requested size, often simply by scaling their
+ * imagery. A client can find the preferred size for some Drawables with
+ * the {@link #getIntrinsicHeight} and {@link #getIntrinsicWidth} methods.
+ *
+ * <li> The {@link #getPadding} method can return from some Drawables
+ * information about how to frame content that is placed inside of them.
+ * For example, a Drawable that is intended to be the frame for a button
+ * widget would need to return padding that correctly places the label
+ * inside of itself.
+ *
+ * <li> The {@link #setState} method allows the client to tell the Drawable
+ * in which state it is to be drawn, such as "focused", "selected", etc.
+ * Some drawables may modify their imagery based on the selected state.
+ *
+ * <li> The {@link #setLevel} method allows the client to supply a single
+ * continuous controller that can modify the Drawable is displayed, such as
+ * a battery level or progress level. Some drawables may modify their
+ * imagery based on the current level.
+ *
+ * <li> A Drawable can perform animations by calling back to its client
+ * through the {@link Callback} interface. All clients should support this
+ * interface (via {@link #setCallback}) so that animations will work. A
+ * simple way to do this is through the system facilities such as
+ * {@link android.view.View#setBackground(Drawable)} and
+ * {@link android.widget.ImageView}.
+ * </ul>
+ *
+ * Though usually not visible to the application, Drawables may take a variety
+ * of forms:
+ *
+ * <ul>
+ * <li> <b>Bitmap</b>: the simplest Drawable, a PNG or JPEG image.
+ * <li> <b>Nine Patch</b>: an extension to the PNG format allows it to
+ * specify information about how to stretch it and place things inside of
+ * it.
+ * <li><b>Vector</b>: a drawable defined in an XML file as a set of points,
+ * lines, and curves along with its associated color information. This type
+ * of drawable can be scaled without loss of display quality.
+ * <li> <b>Shape</b>: contains simple drawing commands instead of a raw
+ * bitmap, allowing it to resize better in some cases.
+ * <li> <b>Layers</b>: a compound drawable, which draws multiple underlying
+ * drawables on top of each other.
+ * <li> <b>States</b>: a compound drawable that selects one of a set of
+ * drawables based on its state.
+ * <li> <b>Levels</b>: a compound drawable that selects one of a set of
+ * drawables based on its level.
+ * <li> <b>Scale</b>: a compound drawable with a single child drawable,
+ * whose overall size is modified based on the current level.
+ * </ul>
+ *
+ * <a name="Custom"></a>
+ * <h3>Custom drawables</h3>
+ *
+ * <p>
+ * All versions of Android allow the Drawable class to be extended and used at
+ * run time in place of framework-provided drawable classes. Starting in
+ * {@link android.os.Build.VERSION_CODES#N API 24}, custom drawables classes
+ * may also be used in XML.
+ * <p>
+ * <strong>Note:</strong> Custom drawable classes are only accessible from
+ * within your application package. Other applications will not be able to load
+ * them.
+ * <p>
+ * At a minimum, custom drawable classes must implement the abstract methods on
+ * Drawable and should override the {@link Drawable#draw(Canvas)} method to
+ * draw content.
+ * <p>
+ * Custom drawables classes may be used in XML in multiple ways:
+ * <ul>
+ * <li>
+ * Using the fully-qualified class name as the XML element name. For
+ * this method, the custom drawable class must be a public top-level
+ * class.
+ * <pre>
+ * <com.myapp.MyCustomDrawable xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:color="#ffff0000" />
+ * </pre>
+ * </li>
+ * <li>
+ * Using <em>drawable</em> as the XML element name and specifying the
+ * fully-qualified class name from the <em>class</em> attribute. This
+ * method may be used for both public top-level classes and public
+ * static inner classes.
+ * <pre>
+ * <drawable xmlns:android="http://schemas.android.com/apk/res/android"
+ * class="com.myapp.MyTopLevelClass$InnerCustomDrawable"
+ * android:color="#ffff0000" />
+ * </pre>
+ * </li>
+ * </ul>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about how to use drawables, read the
+ * <a href="{@docRoot}guide/topics/graphics/2d-graphics.html">Canvas and Drawables</a> developer
+ * guide. For information and examples of creating drawable resources (XML or bitmap files that
+ * can be loaded in code), read the
+ * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>
+ * document.</p></div>
+ */
+public abstract class Drawable {
+ private static final Rect ZERO_BOUNDS_RECT = new Rect();
+
+ static final PorterDuff.Mode DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN;
+
+ private int[] mStateSet = StateSet.WILD_CARD;
+ private int mLevel = 0;
+ private @Config int mChangingConfigurations = 0;
+ private Rect mBounds = ZERO_BOUNDS_RECT; // lazily becomes a new Rect()
+ private WeakReference<Callback> mCallback = null;
+ private boolean mVisible = true;
+
+ private int mLayoutDirection;
+
+ /**
+ * The source density to use when looking up resources using
+ * {@link Resources#getDrawableForDensity(int, int, Theme)}. A value of 0 means there is no
+ * override and the system density will be used.
+ *
+ * NOTE(adamlesinski): This is transient state used to get around the public API that does not
+ * account for source density overrides. Custom drawables implemented by developers do not need
+ * to be aware of the source density override, as it is only used by Launcher to load higher
+ * resolution icons from external Resources packages, which do not execute custom code.
+ * This is all to support the {@link Resources#getDrawableForDensity(int, int, Theme)} API.
+ *
+ * @hide
+ */
+ protected int mSrcDensityOverride = 0;
+
+ /**
+ * Draw in its bounds (set via setBounds) respecting optional effects such
+ * as alpha (set via setAlpha) and color filter (set via setColorFilter).
+ *
+ * @param canvas The canvas to draw into
+ */
+ public abstract void draw(@NonNull Canvas canvas);
+
+ /**
+ * Specify a bounding rectangle for the Drawable. This is where the drawable
+ * will draw when its draw() method is called.
+ */
+ public void setBounds(int left, int top, int right, int bottom) {
+ Rect oldBounds = mBounds;
+
+ if (oldBounds == ZERO_BOUNDS_RECT) {
+ oldBounds = mBounds = new Rect();
+ }
+
+ if (oldBounds.left != left || oldBounds.top != top ||
+ oldBounds.right != right || oldBounds.bottom != bottom) {
+ if (!oldBounds.isEmpty()) {
+ // first invalidate the previous bounds
+ invalidateSelf();
+ }
+ mBounds.set(left, top, right, bottom);
+ onBoundsChange(mBounds);
+ }
+ }
+
+ /**
+ * Specify a bounding rectangle for the Drawable. This is where the drawable
+ * will draw when its draw() method is called.
+ */
+ public void setBounds(@NonNull Rect bounds) {
+ setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ }
+
+ /**
+ * Return a copy of the drawable's bounds in the specified Rect (allocated
+ * by the caller). The bounds specify where this will draw when its draw()
+ * method is called.
+ *
+ * @param bounds Rect to receive the drawable's bounds (allocated by the
+ * caller).
+ */
+ public final void copyBounds(@NonNull Rect bounds) {
+ bounds.set(mBounds);
+ }
+
+ /**
+ * Return a copy of the drawable's bounds in a new Rect. This returns the
+ * same values as getBounds(), but the returned object is guaranteed to not
+ * be changed later by the drawable (i.e. it retains no reference to this
+ * rect). If the caller already has a Rect allocated, call copyBounds(rect).
+ *
+ * @return A copy of the drawable's bounds
+ */
+ @NonNull
+ public final Rect copyBounds() {
+ return new Rect(mBounds);
+ }
+
+ /**
+ * Return the drawable's bounds Rect. Note: for efficiency, the returned
+ * object may be the same object stored in the drawable (though this is not
+ * guaranteed), so if a persistent copy of the bounds is needed, call
+ * copyBounds(rect) instead.
+ * You should also not change the object returned by this method as it may
+ * be the same object stored in the drawable.
+ *
+ * @return The bounds of the drawable (which may change later, so caller
+ * beware). DO NOT ALTER the returned object as it may change the
+ * stored bounds of this drawable.
+ *
+ * @see #copyBounds()
+ * @see #copyBounds(android.graphics.Rect)
+ */
+ @NonNull
+ public final Rect getBounds() {
+ if (mBounds == ZERO_BOUNDS_RECT) {
+ mBounds = new Rect();
+ }
+
+ return mBounds;
+ }
+
+ /**
+ * Return the drawable's dirty bounds Rect. Note: for efficiency, the
+ * returned object may be the same object stored in the drawable (though
+ * this is not guaranteed).
+ * <p>
+ * By default, this returns the full drawable bounds. Custom drawables may
+ * override this method to perform more precise invalidation.
+ *
+ * @return The dirty bounds of this drawable
+ */
+ @NonNull
+ public Rect getDirtyBounds() {
+ return getBounds();
+ }
+
+ /**
+ * Set a mask of the configuration parameters for which this drawable
+ * may change, requiring that it be re-created.
+ *
+ * @param configs A mask of the changing configuration parameters, as
+ * defined by {@link android.content.pm.ActivityInfo}.
+ *
+ * @see android.content.pm.ActivityInfo
+ */
+ public void setChangingConfigurations(@Config int configs) {
+ mChangingConfigurations = configs;
+ }
+
+ /**
+ * Return a mask of the configuration parameters for which this drawable
+ * may change, requiring that it be re-created. The default implementation
+ * returns whatever was provided through
+ * {@link #setChangingConfigurations(int)} or 0 by default. Subclasses
+ * may extend this to or in the changing configurations of any other
+ * drawables they hold.
+ *
+ * @return Returns a mask of the changing configuration parameters, as
+ * defined by {@link android.content.pm.ActivityInfo}.
+ *
+ * @see android.content.pm.ActivityInfo
+ */
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations;
+ }
+
+ /**
+ * Set to true to have the drawable dither its colors when drawn to a
+ * device with fewer than 8-bits per color component.
+ *
+ * @see android.graphics.Paint#setDither(boolean);
+ * @deprecated This property is ignored.
+ */
+ @Deprecated
+ public void setDither(boolean dither) {}
+
+ /**
+ * Set to true to have the drawable filter its bitmaps with bilinear
+ * sampling when they are scaled or rotated.
+ *
+ * <p>This can improve appearance when bitmaps are rotated. If the drawable
+ * does not use bitmaps, this call is ignored.</p>
+ *
+ * @see #isFilterBitmap()
+ * @see android.graphics.Paint#setFilterBitmap(boolean);
+ */
+ public void setFilterBitmap(boolean filter) {}
+
+ /**
+ * @return whether this drawable filters its bitmaps
+ * @see #setFilterBitmap(boolean)
+ */
+ public boolean isFilterBitmap() {
+ return false;
+ }
+
+ /**
+ * Implement this interface if you want to create an animated drawable that
+ * extends {@link android.graphics.drawable.Drawable Drawable}.
+ * Upon retrieving a drawable, use
+ * {@link Drawable#setCallback(android.graphics.drawable.Drawable.Callback)}
+ * to supply your implementation of the interface to the drawable; it uses
+ * this interface to schedule and execute animation changes.
+ */
+ public interface Callback {
+ /**
+ * Called when the drawable needs to be redrawn. A view at this point
+ * should invalidate itself (or at least the part of itself where the
+ * drawable appears).
+ *
+ * @param who The drawable that is requesting the update.
+ */
+ void invalidateDrawable(@NonNull Drawable who);
+
+ /**
+ * A Drawable can call this to schedule the next frame of its
+ * animation. An implementation can generally simply call
+ * {@link android.os.Handler#postAtTime(Runnable, Object, long)} with
+ * the parameters <var>(what, who, when)</var> to perform the
+ * scheduling.
+ *
+ * @param who The drawable being scheduled.
+ * @param what The action to execute.
+ * @param when The time (in milliseconds) to run. The timebase is
+ * {@link android.os.SystemClock#uptimeMillis}
+ */
+ void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);
+
+ /**
+ * A Drawable can call this to unschedule an action previously
+ * scheduled with {@link #scheduleDrawable}. An implementation can
+ * generally simply call
+ * {@link android.os.Handler#removeCallbacks(Runnable, Object)} with
+ * the parameters <var>(what, who)</var> to unschedule the drawable.
+ *
+ * @param who The drawable being unscheduled.
+ * @param what The action being unscheduled.
+ */
+ void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);
+ }
+
+ /**
+ * Bind a {@link Callback} object to this Drawable. Required for clients
+ * that want to support animated drawables.
+ *
+ * @param cb The client's Callback implementation.
+ *
+ * @see #getCallback()
+ */
+ public final void setCallback(@Nullable Callback cb) {
+ mCallback = cb != null ? new WeakReference<>(cb) : null;
+ }
+
+ /**
+ * Return the current {@link Callback} implementation attached to this
+ * Drawable.
+ *
+ * @return A {@link Callback} instance or null if no callback was set.
+ *
+ * @see #setCallback(android.graphics.drawable.Drawable.Callback)
+ */
+ @Nullable
+ public Callback getCallback() {
+ return mCallback != null ? mCallback.get() : null;
+ }
+
+ /**
+ * Use the current {@link Callback} implementation to have this Drawable
+ * redrawn. Does nothing if there is no Callback attached to the
+ * Drawable.
+ *
+ * @see Callback#invalidateDrawable
+ * @see #getCallback()
+ * @see #setCallback(android.graphics.drawable.Drawable.Callback)
+ */
+ public void invalidateSelf() {
+ final Callback callback = getCallback();
+ if (callback != null) {
+ callback.invalidateDrawable(this);
+ }
+ }
+
+ /**
+ * Use the current {@link Callback} implementation to have this Drawable
+ * scheduled. Does nothing if there is no Callback attached to the
+ * Drawable.
+ *
+ * @param what The action being scheduled.
+ * @param when The time (in milliseconds) to run.
+ *
+ * @see Callback#scheduleDrawable
+ */
+ public void scheduleSelf(@NonNull Runnable what, long when) {
+ final Callback callback = getCallback();
+ if (callback != null) {
+ callback.scheduleDrawable(this, what, when);
+ }
+ }
+
+ /**
+ * Use the current {@link Callback} implementation to have this Drawable
+ * unscheduled. Does nothing if there is no Callback attached to the
+ * Drawable.
+ *
+ * @param what The runnable that you no longer want called.
+ *
+ * @see Callback#unscheduleDrawable
+ */
+ public void unscheduleSelf(@NonNull Runnable what) {
+ final Callback callback = getCallback();
+ if (callback != null) {
+ callback.unscheduleDrawable(this, what);
+ }
+ }
+
+ /**
+ * Returns the resolved layout direction for this Drawable.
+ *
+ * @return One of {@link android.view.View#LAYOUT_DIRECTION_LTR},
+ * {@link android.view.View#LAYOUT_DIRECTION_RTL}
+ * @see #setLayoutDirection(int)
+ */
+ public @View.ResolvedLayoutDir int getLayoutDirection() {
+ return mLayoutDirection;
+ }
+
+ /**
+ * Set the layout direction for this drawable. Should be a resolved
+ * layout direction, as the Drawable has no capacity to do the resolution on
+ * its own.
+ *
+ * @param layoutDirection the resolved layout direction for the drawable,
+ * either {@link android.view.View#LAYOUT_DIRECTION_LTR}
+ * or {@link android.view.View#LAYOUT_DIRECTION_RTL}
+ * @return {@code true} if the layout direction change has caused the
+ * appearance of the drawable to change such that it needs to be
+ * re-drawn, {@code false} otherwise
+ * @see #getLayoutDirection()
+ */
+ public final boolean setLayoutDirection(@View.ResolvedLayoutDir int layoutDirection) {
+ if (mLayoutDirection != layoutDirection) {
+ mLayoutDirection = layoutDirection;
+ return onLayoutDirectionChanged(layoutDirection);
+ }
+ return false;
+ }
+
+ /**
+ * Called when the drawable's resolved layout direction changes.
+ *
+ * @param layoutDirection the new resolved layout direction
+ * @return {@code true} if the layout direction change has caused the
+ * appearance of the drawable to change such that it needs to be
+ * re-drawn, {@code false} otherwise
+ * @see #setLayoutDirection(int)
+ */
+ public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
+ return false;
+ }
+
+ /**
+ * Specify an alpha value for the drawable. 0 means fully transparent, and
+ * 255 means fully opaque.
+ */
+ public abstract void setAlpha(@IntRange(from=0,to=255) int alpha);
+
+ /**
+ * Gets the current alpha value for the drawable. 0 means fully transparent,
+ * 255 means fully opaque. This method is implemented by
+ * Drawable subclasses and the value returned is specific to how that class treats alpha.
+ * The default return value is 255 if the class does not override this method to return a value
+ * specific to its use of alpha.
+ */
+ @IntRange(from=0,to=255)
+ public int getAlpha() {
+ return 0xFF;
+ }
+
+ /**
+ * @hide
+ *
+ * Internal-only method for setting xfermode on certain supported drawables.
+ *
+ * Should not be made public since the layers and drawing area with which
+ * Drawables draw is private implementation detail, and not something apps
+ * should rely upon.
+ */
+ public void setXfermode(@Nullable Xfermode mode) {
+ // Base implementation drops it on the floor for compatibility. Whee!
+ }
+
+ /**
+ * Specify an optional color filter for the drawable.
+ * <p>
+ * If a Drawable has a ColorFilter, each output pixel of the Drawable's
+ * drawing contents will be modified by the color filter before it is
+ * blended onto the render target of a Canvas.
+ * </p>
+ * <p>
+ * Pass {@code null} to remove any existing color filter.
+ * </p>
+ * <p class="note"><strong>Note:</strong> Setting a non-{@code null} color
+ * filter disables {@link #setTintList(ColorStateList) tint}.
+ * </p>
+ *
+ * @param colorFilter The color filter to apply, or {@code null} to remove the
+ * existing color filter
+ */
+ public abstract void setColorFilter(@Nullable ColorFilter colorFilter);
+
+ /**
+ * Specify a color and Porter-Duff mode to be the color filter for this
+ * drawable.
+ * <p>
+ * Convenience for {@link #setColorFilter(ColorFilter)} which constructs a
+ * {@link PorterDuffColorFilter}.
+ * </p>
+ * <p class="note"><strong>Note:</strong> Setting a color filter disables
+ * {@link #setTintList(ColorStateList) tint}.
+ * </p>
+ */
+ public void setColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode) {
+ if (getColorFilter() instanceof PorterDuffColorFilter) {
+ PorterDuffColorFilter existing = (PorterDuffColorFilter) getColorFilter();
+ if (existing.getColor() == color && existing.getMode() == mode) {
+ return;
+ }
+ }
+ setColorFilter(new PorterDuffColorFilter(color, mode));
+ }
+
+ /**
+ * Specifies tint color for this drawable.
+ * <p>
+ * A Drawable's drawing content will be blended together with its tint
+ * before it is drawn to the screen. This functions similarly to
+ * {@link #setColorFilter(int, PorterDuff.Mode)}.
+ * </p>
+ * <p>
+ * To clear the tint, pass {@code null} to
+ * {@link #setTintList(ColorStateList)}.
+ * </p>
+ * <p class="note"><strong>Note:</strong> Setting a color filter via
+ * {@link #setColorFilter(ColorFilter)} or
+ * {@link #setColorFilter(int, PorterDuff.Mode)} overrides tint.
+ * </p>
+ *
+ * @param tintColor Color to use for tinting this drawable
+ * @see #setTintList(ColorStateList)
+ * @see #setTintMode(PorterDuff.Mode)
+ */
+ public void setTint(@ColorInt int tintColor) {
+ setTintList(ColorStateList.valueOf(tintColor));
+ }
+
+ /**
+ * Specifies tint color for this drawable as a color state list.
+ * <p>
+ * A Drawable's drawing content will be blended together with its tint
+ * before it is drawn to the screen. This functions similarly to
+ * {@link #setColorFilter(int, PorterDuff.Mode)}.
+ * </p>
+ * <p class="note"><strong>Note:</strong> Setting a color filter via
+ * {@link #setColorFilter(ColorFilter)} or
+ * {@link #setColorFilter(int, PorterDuff.Mode)} overrides tint.
+ * </p>
+ *
+ * @param tint Color state list to use for tinting this drawable, or
+ * {@code null} to clear the tint
+ * @see #setTint(int)
+ * @see #setTintMode(PorterDuff.Mode)
+ */
+ public void setTintList(@Nullable ColorStateList tint) {}
+
+ /**
+ * Specifies a tint blending mode for this drawable.
+ * <p>
+ * Defines how this drawable's tint color should be blended into the drawable
+ * before it is drawn to screen. Default tint mode is {@link PorterDuff.Mode#SRC_IN}.
+ * </p>
+ * <p class="note"><strong>Note:</strong> Setting a color filter via
+ * {@link #setColorFilter(ColorFilter)} or
+ * {@link #setColorFilter(int, PorterDuff.Mode)} overrides tint.
+ * </p>
+ *
+ * @param tintMode A Porter-Duff blending mode
+ * @see #setTint(int)
+ * @see #setTintList(ColorStateList)
+ */
+ public void setTintMode(@NonNull PorterDuff.Mode tintMode) {}
+
+ /**
+ * Returns the current color filter, or {@code null} if none set.
+ *
+ * @return the current color filter, or {@code null} if none set
+ */
+ public @Nullable ColorFilter getColorFilter() {
+ return null;
+ }
+
+ /**
+ * Removes the color filter for this drawable.
+ */
+ public void clearColorFilter() {
+ setColorFilter(null);
+ }
+
+ /**
+ * Specifies the hotspot's location within the drawable.
+ *
+ * @param x The X coordinate of the center of the hotspot
+ * @param y The Y coordinate of the center of the hotspot
+ */
+ public void setHotspot(float x, float y) {}
+
+ /**
+ * Sets the bounds to which the hotspot is constrained, if they should be
+ * different from the drawable bounds.
+ *
+ * @param left position in pixels of the left bound
+ * @param top position in pixels of the top bound
+ * @param right position in pixels of the right bound
+ * @param bottom position in pixels of the bottom bound
+ * @see #getHotspotBounds(android.graphics.Rect)
+ */
+ public void setHotspotBounds(int left, int top, int right, int bottom) {}
+
+ /**
+ * Populates {@code outRect} with the hotspot bounds.
+ *
+ * @param outRect the rect to populate with the hotspot bounds
+ * @see #setHotspotBounds(int, int, int, int)
+ */
+ public void getHotspotBounds(@NonNull Rect outRect) {
+ outRect.set(getBounds());
+ }
+
+ /**
+ * Whether this drawable requests projection.
+ *
+ * @hide magic!
+ */
+ public boolean isProjected() {
+ return false;
+ }
+
+ /**
+ * Indicates whether this drawable will change its appearance based on
+ * state. Clients can use this to determine whether it is necessary to
+ * calculate their state and call setState.
+ *
+ * @return True if this drawable changes its appearance based on state,
+ * false otherwise.
+ * @see #setState(int[])
+ */
+ public boolean isStateful() {
+ return false;
+ }
+
+ /**
+ * Indicates whether this drawable has at least one state spec explicitly
+ * specifying {@link android.R.attr#state_focused}.
+ *
+ * <p>Note: A View uses a {@link Drawable} instance as its background and it
+ * changes its appearance based on a state. On keyboard devices, it should
+ * specify its {@link android.R.attr#state_focused} to make sure the user
+ * knows which view is holding the focus.</p>
+ *
+ * @return {@code true} if {@link android.R.attr#state_focused} is specified
+ * for this drawable.
+ *
+ * @hide
+ */
+ @TestApi
+ public boolean hasFocusStateSpecified() {
+ return false;
+ }
+
+ /**
+ * Specify a set of states for the drawable. These are use-case specific,
+ * so see the relevant documentation. As an example, the background for
+ * widgets like Button understand the following states:
+ * [{@link android.R.attr#state_focused},
+ * {@link android.R.attr#state_pressed}].
+ *
+ * <p>If the new state you are supplying causes the appearance of the
+ * Drawable to change, then it is responsible for calling
+ * {@link #invalidateSelf} in order to have itself redrawn, <em>and</em>
+ * true will be returned from this function.
+ *
+ * <p>Note: The Drawable holds a reference on to <var>stateSet</var>
+ * until a new state array is given to it, so you must not modify this
+ * array during that time.</p>
+ *
+ * @param stateSet The new set of states to be displayed.
+ *
+ * @return Returns true if this change in state has caused the appearance
+ * of the Drawable to change (hence requiring an invalidate), otherwise
+ * returns false.
+ */
+ public boolean setState(@NonNull final int[] stateSet) {
+ if (!Arrays.equals(mStateSet, stateSet)) {
+ mStateSet = stateSet;
+ return onStateChange(stateSet);
+ }
+ return false;
+ }
+
+ /**
+ * Describes the current state, as a union of primitve states, such as
+ * {@link android.R.attr#state_focused},
+ * {@link android.R.attr#state_selected}, etc.
+ * Some drawables may modify their imagery based on the selected state.
+ * @return An array of resource Ids describing the current state.
+ */
+ public @NonNull int[] getState() {
+ return mStateSet;
+ }
+
+ /**
+ * If this Drawable does transition animations between states, ask that
+ * it immediately jump to the current state and skip any active animations.
+ */
+ public void jumpToCurrentState() {
+ }
+
+ /**
+ * @return The current drawable that will be used by this drawable. For simple drawables, this
+ * is just the drawable itself. For drawables that change state like
+ * {@link StateListDrawable} and {@link LevelListDrawable} this will be the child drawable
+ * currently in use.
+ */
+ public @NonNull Drawable getCurrent() {
+ return this;
+ }
+
+ /**
+ * Specify the level for the drawable. This allows a drawable to vary its
+ * imagery based on a continuous controller, for example to show progress
+ * or volume level.
+ *
+ * <p>If the new level you are supplying causes the appearance of the
+ * Drawable to change, then it is responsible for calling
+ * {@link #invalidateSelf} in order to have itself redrawn, <em>and</em>
+ * true will be returned from this function.
+ *
+ * @param level The new level, from 0 (minimum) to 10000 (maximum).
+ *
+ * @return Returns true if this change in level has caused the appearance
+ * of the Drawable to change (hence requiring an invalidate), otherwise
+ * returns false.
+ */
+ public final boolean setLevel(@IntRange(from=0,to=10000) int level) {
+ if (mLevel != level) {
+ mLevel = level;
+ return onLevelChange(level);
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the current level.
+ *
+ * @return int Current level, from 0 (minimum) to 10000 (maximum).
+ */
+ public final @IntRange(from=0,to=10000) int getLevel() {
+ return mLevel;
+ }
+
+ /**
+ * Set whether this Drawable is visible. This generally does not impact
+ * the Drawable's behavior, but is a hint that can be used by some
+ * Drawables, for example, to decide whether run animations.
+ *
+ * @param visible Set to true if visible, false if not.
+ * @param restart You can supply true here to force the drawable to behave
+ * as if it has just become visible, even if it had last
+ * been set visible. Used for example to force animations
+ * to restart.
+ *
+ * @return boolean Returns true if the new visibility is different than
+ * its previous state.
+ */
+ public boolean setVisible(boolean visible, boolean restart) {
+ boolean changed = mVisible != visible;
+ if (changed) {
+ mVisible = visible;
+ invalidateSelf();
+ }
+ return changed;
+ }
+
+ public final boolean isVisible() {
+ return mVisible;
+ }
+
+ /**
+ * Set whether this Drawable is automatically mirrored when its layout direction is RTL
+ * (right-to left). See {@link android.util.LayoutDirection}.
+ *
+ * @param mirrored Set to true if the Drawable should be mirrored, false if not.
+ */
+ public void setAutoMirrored(boolean mirrored) {
+ }
+
+ /**
+ * Tells if this Drawable will be automatically mirrored when its layout direction is RTL
+ * right-to-left. See {@link android.util.LayoutDirection}.
+ *
+ * @return boolean Returns true if this Drawable will be automatically mirrored.
+ */
+ public boolean isAutoMirrored() {
+ return false;
+ }
+
+ /**
+ * Applies the specified theme to this Drawable and its children.
+ *
+ * @param t the theme to apply
+ */
+ public void applyTheme(@NonNull @SuppressWarnings("unused") Theme t) {
+ }
+
+ public boolean canApplyTheme() {
+ return false;
+ }
+
+ /**
+ * Return the opacity/transparency of this Drawable. The returned value is
+ * one of the abstract format constants in
+ * {@link android.graphics.PixelFormat}:
+ * {@link android.graphics.PixelFormat#UNKNOWN},
+ * {@link android.graphics.PixelFormat#TRANSLUCENT},
+ * {@link android.graphics.PixelFormat#TRANSPARENT}, or
+ * {@link android.graphics.PixelFormat#OPAQUE}.
+ *
+ * <p>An OPAQUE drawable is one that draws all all content within its bounds, completely
+ * covering anything behind the drawable. A TRANSPARENT drawable is one that draws nothing
+ * within its bounds, allowing everything behind it to show through. A TRANSLUCENT drawable
+ * is a drawable in any other state, where the drawable will draw some, but not all,
+ * of the content within its bounds and at least some content behind the drawable will
+ * be visible. If the visibility of the drawable's contents cannot be determined, the
+ * safest/best return value is TRANSLUCENT.
+ *
+ * <p>Generally a Drawable should be as conservative as possible with the
+ * value it returns. For example, if it contains multiple child drawables
+ * and only shows one of them at a time, if only one of the children is
+ * TRANSLUCENT and the others are OPAQUE then TRANSLUCENT should be
+ * returned. You can use the method {@link #resolveOpacity} to perform a
+ * standard reduction of two opacities to the appropriate single output.
+ *
+ * <p>Note that the returned value does not necessarily take into account a
+ * custom alpha or color filter that has been applied by the client through
+ * the {@link #setAlpha} or {@link #setColorFilter} methods. Some subclasses,
+ * such as {@link BitmapDrawable}, {@link ColorDrawable}, and {@link GradientDrawable},
+ * do account for the value of {@link #setAlpha}, but the general behavior is dependent
+ * upon the implementation of the subclass.
+ *
+ * @return int The opacity class of the Drawable.
+ *
+ * @see android.graphics.PixelFormat
+ */
+ public abstract @PixelFormat.Opacity int getOpacity();
+
+ /**
+ * Return the appropriate opacity value for two source opacities. If
+ * either is UNKNOWN, that is returned; else, if either is TRANSLUCENT,
+ * that is returned; else, if either is TRANSPARENT, that is returned;
+ * else, OPAQUE is returned.
+ *
+ * <p>This is to help in implementing {@link #getOpacity}.
+ *
+ * @param op1 One opacity value.
+ * @param op2 Another opacity value.
+ *
+ * @return int The combined opacity value.
+ *
+ * @see #getOpacity
+ */
+ public static @PixelFormat.Opacity int resolveOpacity(@PixelFormat.Opacity int op1,
+ @PixelFormat.Opacity int op2) {
+ if (op1 == op2) {
+ return op1;
+ }
+ if (op1 == PixelFormat.UNKNOWN || op2 == PixelFormat.UNKNOWN) {
+ return PixelFormat.UNKNOWN;
+ }
+ if (op1 == PixelFormat.TRANSLUCENT || op2 == PixelFormat.TRANSLUCENT) {
+ return PixelFormat.TRANSLUCENT;
+ }
+ if (op1 == PixelFormat.TRANSPARENT || op2 == PixelFormat.TRANSPARENT) {
+ return PixelFormat.TRANSPARENT;
+ }
+ return PixelFormat.OPAQUE;
+ }
+
+ /**
+ * Returns a Region representing the part of the Drawable that is completely
+ * transparent. This can be used to perform drawing operations, identifying
+ * which parts of the target will not change when rendering the Drawable.
+ * The default implementation returns null, indicating no transparent
+ * region; subclasses can optionally override this to return an actual
+ * Region if they want to supply this optimization information, but it is
+ * not required that they do so.
+ *
+ * @return Returns null if the Drawables has no transparent region to
+ * report, else a Region holding the parts of the Drawable's bounds that
+ * are transparent.
+ */
+ public @Nullable Region getTransparentRegion() {
+ return null;
+ }
+
+ /**
+ * Override this in your subclass to change appearance if you recognize the
+ * specified state.
+ *
+ * @return Returns true if the state change has caused the appearance of
+ * the Drawable to change (that is, it needs to be drawn), else false
+ * if it looks the same and there is no need to redraw it since its
+ * last state.
+ */
+ protected boolean onStateChange(int[] state) {
+ return false;
+ }
+
+ /** Override this in your subclass to change appearance if you vary based
+ * on level.
+ * @return Returns true if the level change has caused the appearance of
+ * the Drawable to change (that is, it needs to be drawn), else false
+ * if it looks the same and there is no need to redraw it since its
+ * last level.
+ */
+ protected boolean onLevelChange(int level) {
+ return false;
+ }
+
+ /**
+ * Override this in your subclass to change appearance if you vary based on
+ * the bounds.
+ */
+ protected void onBoundsChange(Rect bounds) {
+ // Stub method.
+ }
+
+ /**
+ * Returns the drawable's intrinsic width.
+ * <p>
+ * Intrinsic width is the width at which the drawable would like to be laid
+ * out, including any inherent padding. If the drawable has no intrinsic
+ * width, such as a solid color, this method returns -1.
+ *
+ * @return the intrinsic width, or -1 if no intrinsic width
+ */
+ public int getIntrinsicWidth() {
+ return -1;
+ }
+
+ /**
+ * Returns the drawable's intrinsic height.
+ * <p>
+ * Intrinsic height is the height at which the drawable would like to be
+ * laid out, including any inherent padding. If the drawable has no
+ * intrinsic height, such as a solid color, this method returns -1.
+ *
+ * @return the intrinsic height, or -1 if no intrinsic height
+ */
+ public int getIntrinsicHeight() {
+ return -1;
+ }
+
+ /**
+ * Returns the minimum width suggested by this Drawable. If a View uses this
+ * Drawable as a background, it is suggested that the View use at least this
+ * value for its width. (There will be some scenarios where this will not be
+ * possible.) This value should INCLUDE any padding.
+ *
+ * @return The minimum width suggested by this Drawable. If this Drawable
+ * doesn't have a suggested minimum width, 0 is returned.
+ */
+ public int getMinimumWidth() {
+ final int intrinsicWidth = getIntrinsicWidth();
+ return intrinsicWidth > 0 ? intrinsicWidth : 0;
+ }
+
+ /**
+ * Returns the minimum height suggested by this Drawable. If a View uses this
+ * Drawable as a background, it is suggested that the View use at least this
+ * value for its height. (There will be some scenarios where this will not be
+ * possible.) This value should INCLUDE any padding.
+ *
+ * @return The minimum height suggested by this Drawable. If this Drawable
+ * doesn't have a suggested minimum height, 0 is returned.
+ */
+ public int getMinimumHeight() {
+ final int intrinsicHeight = getIntrinsicHeight();
+ return intrinsicHeight > 0 ? intrinsicHeight : 0;
+ }
+
+ /**
+ * Return in padding the insets suggested by this Drawable for placing
+ * content inside the drawable's bounds. Positive values move toward the
+ * center of the Drawable (set Rect.inset).
+ *
+ * @return true if this drawable actually has a padding, else false. When false is returned,
+ * the padding is always set to 0.
+ */
+ public boolean getPadding(@NonNull Rect padding) {
+ padding.set(0, 0, 0, 0);
+ return false;
+ }
+
+ /**
+ * Return in insets the layout insets suggested by this Drawable for use with alignment
+ * operations during layout.
+ *
+ * @hide
+ */
+ public @NonNull Insets getOpticalInsets() {
+ return Insets.NONE;
+ }
+
+ /**
+ * Called to get the drawable to populate the Outline that defines its drawing area.
+ * <p>
+ * This method is called by the default {@link android.view.ViewOutlineProvider} to define
+ * the outline of the View.
+ * <p>
+ * The default behavior defines the outline to be the bounding rectangle of 0 alpha.
+ * Subclasses that wish to convey a different shape or alpha value must override this method.
+ *
+ * @see android.view.View#setOutlineProvider(android.view.ViewOutlineProvider)
+ */
+ public void getOutline(@NonNull Outline outline) {
+ outline.setRect(getBounds());
+ outline.setAlpha(0);
+ }
+
+ /**
+ * Make this drawable mutable. This operation cannot be reversed. A mutable
+ * drawable is guaranteed to not share its state with any other drawable.
+ * This is especially useful when you need to modify properties of drawables
+ * loaded from resources. By default, all drawables instances loaded from
+ * the same resource share a common state; if you modify the state of one
+ * instance, all the other instances will receive the same modification.
+ *
+ * Calling this method on a mutable Drawable will have no effect.
+ *
+ * @return This drawable.
+ * @see ConstantState
+ * @see #getConstantState()
+ */
+ public @NonNull Drawable mutate() {
+ return this;
+ }
+
+ /**
+ * Clears the mutated state, allowing this drawable to be cached and
+ * mutated again.
+ * <p>
+ * This is hidden because only framework drawables can be cached, so
+ * custom drawables don't need to support constant state, mutate(), or
+ * clearMutated().
+ *
+ * @hide
+ */
+ public void clearMutated() {
+ // Default implementation is no-op.
+ }
+
+ /**
+ * Create a drawable from an inputstream
+ */
+ public static Drawable createFromStream(InputStream is, String srcName) {
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, srcName != null ? srcName : "Unknown drawable");
+ try {
+ return createFromResourceStream(null, null, is, srcName);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ /**
+ * Create a drawable from an inputstream, using the given resources and
+ * value to determine density information.
+ */
+ public static Drawable createFromResourceStream(Resources res, TypedValue value,
+ InputStream is, String srcName) {
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, srcName != null ? srcName : "Unknown drawable");
+ try {
+ return createFromResourceStream(res, value, is, srcName, null);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ /**
+ * Create a drawable from an inputstream, using the given resources and
+ * value to determine density information.
+ */
+ public static Drawable createFromResourceStream(Resources res, TypedValue value,
+ InputStream is, String srcName, BitmapFactory.Options opts) {
+ if (is == null) {
+ return null;
+ }
+
+ /* ugh. The decodeStream contract is that we have already allocated
+ the pad rect, but if the bitmap does not had a ninepatch chunk,
+ then the pad will be ignored. If we could change this to lazily
+ alloc/assign the rect, we could avoid the GC churn of making new
+ Rects only to drop them on the floor.
+ */
+ Rect pad = new Rect();
+
+ // Special stuff for compatibility mode: if the target density is not
+ // the same as the display density, but the resource -is- the same as
+ // the display density, then don't scale it down to the target density.
+ // This allows us to load the system's density-correct resources into
+ // an application in compatibility mode, without scaling those down
+ // to the compatibility density only to have them scaled back up when
+ // drawn to the screen.
+ if (opts == null) opts = new BitmapFactory.Options();
+ opts.inScreenDensity = Drawable.resolveDensity(res, 0);
+ Bitmap bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
+ if (bm != null) {
+ byte[] np = bm.getNinePatchChunk();
+ if (np == null || !NinePatch.isNinePatchChunk(np)) {
+ np = null;
+ pad = null;
+ }
+
+ final Rect opticalInsets = new Rect();
+ bm.getOpticalInsets(opticalInsets);
+ return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
+ }
+ return null;
+ }
+
+ /**
+ * Create a drawable from an XML document. For more information on how to
+ * create resources in XML, see
+ * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
+ */
+ @NonNull
+ public static Drawable createFromXml(@NonNull Resources r, @NonNull XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ return createFromXml(r, parser, null);
+ }
+
+ /**
+ * Create a drawable from an XML document using an optional {@link Theme}.
+ * For more information on how to create resources in XML, see
+ * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
+ */
+ @NonNull
+ public static Drawable createFromXml(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @Nullable Theme theme) throws XmlPullParserException, IOException {
+ return createFromXmlForDensity(r, parser, 0, theme);
+ }
+
+ /**
+ * Version of {@link #createFromXml(Resources, XmlPullParser, Theme)} that accepts a density
+ * override.
+ * @hide
+ */
+ @NonNull
+ public static Drawable createFromXmlForDensity(@NonNull Resources r,
+ @NonNull XmlPullParser parser, int density, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ //noinspection StatementWithEmptyBody
+ while ((type=parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Empty loop.
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException("No start tag found");
+ }
+
+ Drawable drawable = createFromXmlInnerForDensity(r, parser, attrs, density, theme);
+
+ if (drawable == null) {
+ throw new RuntimeException("Unknown initial tag: " + parser.getName());
+ }
+
+ return drawable;
+ }
+
+ /**
+ * Create from inside an XML document. Called on a parser positioned at
+ * a tag in an XML document, tries to create a Drawable from that tag.
+ * Returns null if the tag is not a valid drawable.
+ */
+ @NonNull
+ public static Drawable createFromXmlInner(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs) throws XmlPullParserException, IOException {
+ return createFromXmlInner(r, parser, attrs, null);
+ }
+
+ /**
+ * Create a drawable from inside an XML document using an optional
+ * {@link Theme}. Called on a parser positioned at a tag in an XML
+ * document, tries to create a Drawable from that tag. Returns {@code null}
+ * if the tag is not a valid drawable.
+ */
+ @NonNull
+ public static Drawable createFromXmlInner(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ return createFromXmlInnerForDensity(r, parser, attrs, 0, theme);
+ }
+
+ /**
+ * Version of {@link #createFromXmlInner(Resources, XmlPullParser, AttributeSet, Theme)} that
+ * accepts an override density.
+ */
+ @NonNull
+ static Drawable createFromXmlInnerForDensity(@NonNull Resources r,
+ @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density,
+ @Nullable Theme theme) throws XmlPullParserException, IOException {
+ return r.getDrawableInflater().inflateFromXmlForDensity(parser.getName(), parser, attrs,
+ density, theme);
+ }
+
+ /**
+ * Create a drawable from file path name.
+ */
+ @Nullable
+ public static Drawable createFromPath(String pathName) {
+ if (pathName == null) {
+ return null;
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, pathName);
+ try {
+ Bitmap bm = BitmapFactory.decodeFile(pathName);
+ if (bm != null) {
+ return drawableFromBitmap(null, bm, null, null, null, pathName);
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+
+ return null;
+ }
+
+ /**
+ * Inflate this Drawable from an XML resource. Does not apply a theme.
+ *
+ * @see #inflate(Resources, XmlPullParser, AttributeSet, Theme)
+ */
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs) throws XmlPullParserException, IOException {
+ inflate(r, parser, attrs, null);
+ }
+
+ /**
+ * Inflate this Drawable from an XML resource optionally styled by a theme.
+ * This can't be called more than once for each Drawable. Note that framework may have called
+ * this once to create the Drawable instance from XML resource.
+ *
+ * @param r Resources used to resolve attribute values
+ * @param parser XML parser from which to inflate this Drawable
+ * @param attrs Base set of attribute values
+ * @param theme Theme to apply, may be null
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.Drawable);
+ mVisible = a.getBoolean(R.styleable.Drawable_visible, mVisible);
+ a.recycle();
+ }
+
+ /**
+ * Inflate a Drawable from an XML resource.
+ *
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ void inflateWithAttributes(@NonNull @SuppressWarnings("unused") Resources r,
+ @NonNull @SuppressWarnings("unused") XmlPullParser parser, @NonNull TypedArray attrs,
+ @AttrRes int visibleAttr) throws XmlPullParserException, IOException {
+ mVisible = attrs.getBoolean(visibleAttr, mVisible);
+ }
+
+ /**
+ * Sets the source override density for this Drawable. If non-zero, this density is to be used
+ * for any calls to {@link Resources#getDrawableForDensity(int, int, Theme)} or
+ * {@link Resources#getValueForDensity(int, int, TypedValue, boolean)}.
+ * @hide
+ */
+ final void setSrcDensityOverride(int density) {
+ mSrcDensityOverride = density;
+ }
+
+ /**
+ * This abstract class is used by {@link Drawable}s to store shared constant state and data
+ * between Drawables. {@link BitmapDrawable}s created from the same resource will for instance
+ * share a unique bitmap stored in their ConstantState.
+ *
+ * <p>
+ * {@link #newDrawable(Resources)} can be used as a factory to create new Drawable instances
+ * from this ConstantState.
+ * </p>
+ *
+ * Use {@link Drawable#getConstantState()} to retrieve the ConstantState of a Drawable. Calling
+ * {@link Drawable#mutate()} on a Drawable should typically create a new ConstantState for that
+ * Drawable.
+ */
+ public static abstract class ConstantState {
+ /**
+ * Creates a new Drawable instance from its constant state.
+ * <p>
+ * <strong>Note:</strong> Using this method means density-dependent
+ * properties, such as pixel dimensions or bitmap images, will not be
+ * updated to match the density of the target display. To ensure
+ * correct scaling, use {@link #newDrawable(Resources)} instead to
+ * provide an appropriate Resources object.
+ *
+ * @return a new drawable object based on this constant state
+ * @see #newDrawable(Resources)
+ */
+ public abstract @NonNull Drawable newDrawable();
+
+ /**
+ * Creates a new Drawable instance from its constant state using the
+ * specified resources. This method should be implemented for drawables
+ * that have density-dependent properties.
+ * <p>
+ * The default implementation for this method calls through to
+ * {@link #newDrawable()}.
+ *
+ * @param res the resources of the context in which the drawable will
+ * be displayed
+ * @return a new drawable object based on this constant state
+ */
+ public @NonNull Drawable newDrawable(@Nullable Resources res) {
+ return newDrawable();
+ }
+
+ /**
+ * Creates a new Drawable instance from its constant state using the
+ * specified resources and theme. This method should be implemented for
+ * drawables that have theme-dependent properties.
+ * <p>
+ * The default implementation for this method calls through to
+ * {@link #newDrawable(Resources)}.
+ *
+ * @param res the resources of the context in which the drawable will
+ * be displayed
+ * @param theme the theme of the context in which the drawable will be
+ * displayed
+ * @return a new drawable object based on this constant state
+ */
+ public @NonNull Drawable newDrawable(@Nullable Resources res,
+ @Nullable @SuppressWarnings("unused") Theme theme) {
+ return newDrawable(res);
+ }
+
+ /**
+ * Return a bit mask of configuration changes that will impact
+ * this drawable (and thus require completely reloading it).
+ */
+ public abstract @Config int getChangingConfigurations();
+
+ /**
+ * Return whether this constant state can have a theme applied.
+ */
+ public boolean canApplyTheme() {
+ return false;
+ }
+ }
+
+ /**
+ * Return a {@link ConstantState} instance that holds the shared state of this Drawable.
+ *
+ * @return The ConstantState associated to that Drawable.
+ * @see ConstantState
+ * @see Drawable#mutate()
+ */
+ public @Nullable ConstantState getConstantState() {
+ return null;
+ }
+
+ private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np,
+ Rect pad, Rect layoutBounds, String srcName) {
+
+ if (np != null) {
+ return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName);
+ }
+
+ return new BitmapDrawable(res, bm);
+ }
+
+ /**
+ * Ensures the tint filter is consistent with the current tint color and
+ * mode.
+ */
+ @Nullable PorterDuffColorFilter updateTintFilter(@Nullable PorterDuffColorFilter tintFilter,
+ @Nullable ColorStateList tint, @Nullable PorterDuff.Mode tintMode) {
+ if (tint == null || tintMode == null) {
+ return null;
+ }
+
+ final int color = tint.getColorForState(getState(), Color.TRANSPARENT);
+ if (tintFilter == null) {
+ return new PorterDuffColorFilter(color, tintMode);
+ }
+
+ tintFilter.setColor(color);
+ tintFilter.setMode(tintMode);
+ return tintFilter;
+ }
+
+ /**
+ * Obtains styled attributes from the theme, if available, or unstyled
+ * resources if the theme is null.
+ * @hide
+ */
+ protected static @NonNull TypedArray obtainAttributes(@NonNull Resources res,
+ @Nullable Theme theme, @NonNull AttributeSet set, @NonNull int[] attrs) {
+ if (theme == null) {
+ return res.obtainAttributes(set, attrs);
+ }
+ return theme.obtainStyledAttributes(set, attrs, 0, 0);
+ }
+
+ /**
+ * Scales a floating-point pixel value from the source density to the
+ * target density.
+ *
+ * @param pixels the pixel value for use in source density
+ * @param sourceDensity the source density
+ * @param targetDensity the target density
+ * @return the scaled pixel value for use in target density
+ */
+ static float scaleFromDensity(float pixels, int sourceDensity, int targetDensity) {
+ return pixels * targetDensity / sourceDensity;
+ }
+
+ /**
+ * Scales a pixel value from the source density to the target density,
+ * optionally handling the resulting pixel value as a size rather than an
+ * offset.
+ * <p>
+ * A size conversion involves rounding the base value and ensuring that
+ * a non-zero base value is at least one pixel in size.
+ * <p>
+ * An offset conversion involves simply truncating the base value to an
+ * integer.
+ *
+ * @param pixels the pixel value for use in source density
+ * @param sourceDensity the source density
+ * @param targetDensity the target density
+ * @param isSize {@code true} to handle the resulting scaled value as a
+ * size, or {@code false} to handle it as an offset
+ * @return the scaled pixel value for use in target density
+ */
+ static int scaleFromDensity(
+ int pixels, int sourceDensity, int targetDensity, boolean isSize) {
+ if (pixels == 0 || sourceDensity == targetDensity) {
+ return pixels;
+ }
+
+ final float result = pixels * targetDensity / (float) sourceDensity;
+ if (!isSize) {
+ return (int) result;
+ }
+
+ final int rounded = Math.round(result);
+ if (rounded != 0) {
+ return rounded;
+ } else if (pixels > 0) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+
+ static int resolveDensity(@Nullable Resources r, int parentDensity) {
+ final int densityDpi = r == null ? parentDensity : r.getDisplayMetrics().densityDpi;
+ return densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
+ }
+
+ /**
+ * Re-throws an exception as a {@link RuntimeException} with an empty stack
+ * trace to avoid cluttering the log. The original exception's stack trace
+ * will still be included.
+ *
+ * @param cause the exception to re-throw
+ * @throws RuntimeException
+ */
+ static void rethrowAsRuntimeException(@NonNull Exception cause) throws RuntimeException {
+ final RuntimeException e = new RuntimeException(cause);
+ e.setStackTrace(new StackTraceElement[0]);
+ throw e;
+ }
+
+ /**
+ * Parses a {@link android.graphics.PorterDuff.Mode} from a tintMode
+ * attribute's enum value.
+ *
+ * @hide
+ */
+ public static PorterDuff.Mode parseTintMode(int value, Mode defaultMode) {
+ switch (value) {
+ case 3: return Mode.SRC_OVER;
+ case 5: return Mode.SRC_IN;
+ case 9: return Mode.SRC_ATOP;
+ case 14: return Mode.MULTIPLY;
+ case 15: return Mode.SCREEN;
+ case 16: return Mode.ADD;
+ default: return defaultMode;
+ }
+ }
+}
+
diff --git a/android/graphics/drawable/DrawableContainer.java b/android/graphics/drawable/DrawableContainer.java
new file mode 100644
index 0000000..aa4cd9c
--- /dev/null
+++ b/android/graphics/drawable/DrawableContainer.java
@@ -0,0 +1,1277 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import android.annotation.NonNull;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Insets;
+import android.graphics.Outline;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.util.LayoutDirection;
+import android.util.SparseArray;
+import android.view.View;
+
+/**
+ * A helper class that contains several {@link Drawable}s and selects which one to use.
+ *
+ * You can subclass it to create your own DrawableContainers or directly use one its child classes.
+ */
+public class DrawableContainer extends Drawable implements Drawable.Callback {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "DrawableContainer";
+
+ /**
+ * To be proper, we should have a getter for dither (and alpha, etc.)
+ * so that proxy classes like this can save/restore their delegates'
+ * values, but we don't have getters. Since we do have setters
+ * (e.g. setDither), which this proxy forwards on, we have to have some
+ * default/initial setting.
+ *
+ * The initial setting for dither is now true, since it almost always seems
+ * to improve the quality at negligible cost.
+ */
+ private static final boolean DEFAULT_DITHER = true;
+ private DrawableContainerState mDrawableContainerState;
+ private Rect mHotspotBounds;
+ private Drawable mCurrDrawable;
+ private Drawable mLastDrawable;
+ private int mAlpha = 0xFF;
+
+ /** Whether setAlpha() has been called at least once. */
+ private boolean mHasAlpha;
+
+ private int mCurIndex = -1;
+ private int mLastIndex = -1;
+ private boolean mMutated;
+
+ // Animations.
+ private Runnable mAnimationRunnable;
+ private long mEnterAnimationEnd;
+ private long mExitAnimationEnd;
+
+ /** Callback that blocks invalidation. Used for drawable initialization. */
+ private BlockInvalidateCallback mBlockInvalidateCallback;
+
+ // overrides from Drawable
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mCurrDrawable != null) {
+ mCurrDrawable.draw(canvas);
+ }
+ if (mLastDrawable != null) {
+ mLastDrawable.draw(canvas);
+ }
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations()
+ | mDrawableContainerState.getChangingConfigurations();
+ }
+
+ private boolean needsMirroring() {
+ return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL;
+ }
+
+ @Override
+ public boolean getPadding(Rect padding) {
+ final Rect r = mDrawableContainerState.getConstantPadding();
+ boolean result;
+ if (r != null) {
+ padding.set(r);
+ result = (r.left | r.top | r.bottom | r.right) != 0;
+ } else {
+ if (mCurrDrawable != null) {
+ result = mCurrDrawable.getPadding(padding);
+ } else {
+ result = super.getPadding(padding);
+ }
+ }
+ if (needsMirroring()) {
+ final int left = padding.left;
+ final int right = padding.right;
+ padding.left = right;
+ padding.right = left;
+ }
+ return result;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public Insets getOpticalInsets() {
+ if (mCurrDrawable != null) {
+ return mCurrDrawable.getOpticalInsets();
+ }
+ return Insets.NONE;
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ if (mCurrDrawable != null) {
+ mCurrDrawable.getOutline(outline);
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (!mHasAlpha || mAlpha != alpha) {
+ mHasAlpha = true;
+ mAlpha = alpha;
+ if (mCurrDrawable != null) {
+ if (mEnterAnimationEnd == 0) {
+ mCurrDrawable.setAlpha(alpha);
+ } else {
+ animate(false);
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public void setDither(boolean dither) {
+ if (mDrawableContainerState.mDither != dither) {
+ mDrawableContainerState.mDither = dither;
+ if (mCurrDrawable != null) {
+ mCurrDrawable.setDither(mDrawableContainerState.mDither);
+ }
+ }
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mDrawableContainerState.mHasColorFilter = true;
+
+ if (mDrawableContainerState.mColorFilter != colorFilter) {
+ mDrawableContainerState.mColorFilter = colorFilter;
+
+ if (mCurrDrawable != null) {
+ mCurrDrawable.setColorFilter(colorFilter);
+ }
+ }
+ }
+
+ @Override
+ public void setTintList(ColorStateList tint) {
+ mDrawableContainerState.mHasTintList = true;
+
+ if (mDrawableContainerState.mTintList != tint) {
+ mDrawableContainerState.mTintList = tint;
+
+ if (mCurrDrawable != null) {
+ mCurrDrawable.setTintList(tint);
+ }
+ }
+ }
+
+ @Override
+ public void setTintMode(Mode tintMode) {
+ mDrawableContainerState.mHasTintMode = true;
+
+ if (mDrawableContainerState.mTintMode != tintMode) {
+ mDrawableContainerState.mTintMode = tintMode;
+
+ if (mCurrDrawable != null) {
+ mCurrDrawable.setTintMode(tintMode);
+ }
+ }
+ }
+
+ /**
+ * Change the global fade duration when a new drawable is entering
+ * the scene.
+ *
+ * @param ms The amount of time to fade in milliseconds.
+ */
+ public void setEnterFadeDuration(int ms) {
+ mDrawableContainerState.mEnterFadeDuration = ms;
+ }
+
+ /**
+ * Change the global fade duration when a new drawable is leaving
+ * the scene.
+ *
+ * @param ms The amount of time to fade in milliseconds.
+ */
+ public void setExitFadeDuration(int ms) {
+ mDrawableContainerState.mExitFadeDuration = ms;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ if (mLastDrawable != null) {
+ mLastDrawable.setBounds(bounds);
+ }
+ if (mCurrDrawable != null) {
+ mCurrDrawable.setBounds(bounds);
+ }
+ }
+
+ @Override
+ public boolean isStateful() {
+ return mDrawableContainerState.isStateful();
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ if (mCurrDrawable != null) {
+ return mCurrDrawable.hasFocusStateSpecified();
+ }
+ if (mLastDrawable != null) {
+ return mLastDrawable.hasFocusStateSpecified();
+ }
+ return false;
+ }
+
+ @Override
+ public void setAutoMirrored(boolean mirrored) {
+ if (mDrawableContainerState.mAutoMirrored != mirrored) {
+ mDrawableContainerState.mAutoMirrored = mirrored;
+ if (mCurrDrawable != null) {
+ mCurrDrawable.setAutoMirrored(mDrawableContainerState.mAutoMirrored);
+ }
+ }
+ }
+
+ @Override
+ public boolean isAutoMirrored() {
+ return mDrawableContainerState.mAutoMirrored;
+ }
+
+ @Override
+ public void jumpToCurrentState() {
+ boolean changed = false;
+ if (mLastDrawable != null) {
+ mLastDrawable.jumpToCurrentState();
+ mLastDrawable = null;
+ mLastIndex = -1;
+ changed = true;
+ }
+ if (mCurrDrawable != null) {
+ mCurrDrawable.jumpToCurrentState();
+ if (mHasAlpha) {
+ mCurrDrawable.setAlpha(mAlpha);
+ }
+ }
+ if (mExitAnimationEnd != 0) {
+ mExitAnimationEnd = 0;
+ changed = true;
+ }
+ if (mEnterAnimationEnd != 0) {
+ mEnterAnimationEnd = 0;
+ changed = true;
+ }
+ if (changed) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setHotspot(float x, float y) {
+ if (mCurrDrawable != null) {
+ mCurrDrawable.setHotspot(x, y);
+ }
+ }
+
+ @Override
+ public void setHotspotBounds(int left, int top, int right, int bottom) {
+ if (mHotspotBounds == null) {
+ mHotspotBounds = new Rect(left, top, right, bottom);
+ } else {
+ mHotspotBounds.set(left, top, right, bottom);
+ }
+
+ if (mCurrDrawable != null) {
+ mCurrDrawable.setHotspotBounds(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public void getHotspotBounds(Rect outRect) {
+ if (mHotspotBounds != null) {
+ outRect.set(mHotspotBounds);
+ } else {
+ super.getHotspotBounds(outRect);
+ }
+ }
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ if (mLastDrawable != null) {
+ return mLastDrawable.setState(state);
+ }
+ if (mCurrDrawable != null) {
+ return mCurrDrawable.setState(state);
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ if (mLastDrawable != null) {
+ return mLastDrawable.setLevel(level);
+ }
+ if (mCurrDrawable != null) {
+ return mCurrDrawable.setLevel(level);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
+ // Let the container handle setting its own layout direction. Otherwise,
+ // we're accessing potentially unused states.
+ return mDrawableContainerState.setLayoutDirection(layoutDirection, getCurrentIndex());
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ if (mDrawableContainerState.isConstantSize()) {
+ return mDrawableContainerState.getConstantWidth();
+ }
+ return mCurrDrawable != null ? mCurrDrawable.getIntrinsicWidth() : -1;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ if (mDrawableContainerState.isConstantSize()) {
+ return mDrawableContainerState.getConstantHeight();
+ }
+ return mCurrDrawable != null ? mCurrDrawable.getIntrinsicHeight() : -1;
+ }
+
+ @Override
+ public int getMinimumWidth() {
+ if (mDrawableContainerState.isConstantSize()) {
+ return mDrawableContainerState.getConstantMinimumWidth();
+ }
+ return mCurrDrawable != null ? mCurrDrawable.getMinimumWidth() : 0;
+ }
+
+ @Override
+ public int getMinimumHeight() {
+ if (mDrawableContainerState.isConstantSize()) {
+ return mDrawableContainerState.getConstantMinimumHeight();
+ }
+ return mCurrDrawable != null ? mCurrDrawable.getMinimumHeight() : 0;
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable who) {
+ // This may have been called as the result of a tint changing, in
+ // which case we may need to refresh the cached statefulness or
+ // opacity.
+ if (mDrawableContainerState != null) {
+ mDrawableContainerState.invalidateCache();
+ }
+
+ if (who == mCurrDrawable && getCallback() != null) {
+ getCallback().invalidateDrawable(this);
+ }
+ }
+
+ @Override
+ public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+ if (who == mCurrDrawable && getCallback() != null) {
+ getCallback().scheduleDrawable(this, what, when);
+ }
+ }
+
+ @Override
+ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+ if (who == mCurrDrawable && getCallback() != null) {
+ getCallback().unscheduleDrawable(this, what);
+ }
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ boolean changed = super.setVisible(visible, restart);
+ if (mLastDrawable != null) {
+ mLastDrawable.setVisible(visible, restart);
+ }
+ if (mCurrDrawable != null) {
+ mCurrDrawable.setVisible(visible, restart);
+ }
+ return changed;
+ }
+
+ @Override
+ public int getOpacity() {
+ return mCurrDrawable == null || !mCurrDrawable.isVisible() ? PixelFormat.TRANSPARENT :
+ mDrawableContainerState.getOpacity();
+ }
+
+ /** @hide */
+ public void setCurrentIndex(int index) {
+ selectDrawable(index);
+ }
+
+ /** @hide */
+ public int getCurrentIndex() {
+ return mCurIndex;
+ }
+
+ /**
+ * Sets the currently displayed drawable by index.
+ * <p>
+ * If an invalid index is specified, the current drawable will be set to
+ * {@code null} and the index will be set to {@code -1}.
+ *
+ * @param index the index of the drawable to display
+ * @return {@code true} if the drawable changed, {@code false} otherwise
+ */
+ public boolean selectDrawable(int index) {
+ if (index == mCurIndex) {
+ return false;
+ }
+
+ final long now = SystemClock.uptimeMillis();
+
+ if (DEBUG) android.util.Log.i(TAG, toString() + " from " + mCurIndex + " to " + index
+ + ": exit=" + mDrawableContainerState.mExitFadeDuration
+ + " enter=" + mDrawableContainerState.mEnterFadeDuration);
+
+ if (mDrawableContainerState.mExitFadeDuration > 0) {
+ if (mLastDrawable != null) {
+ mLastDrawable.setVisible(false, false);
+ }
+ if (mCurrDrawable != null) {
+ mLastDrawable = mCurrDrawable;
+ mLastIndex = mCurIndex;
+ mExitAnimationEnd = now + mDrawableContainerState.mExitFadeDuration;
+ } else {
+ mLastDrawable = null;
+ mLastIndex = -1;
+ mExitAnimationEnd = 0;
+ }
+ } else if (mCurrDrawable != null) {
+ mCurrDrawable.setVisible(false, false);
+ }
+
+ if (index >= 0 && index < mDrawableContainerState.mNumChildren) {
+ final Drawable d = mDrawableContainerState.getChild(index);
+ mCurrDrawable = d;
+ mCurIndex = index;
+ if (d != null) {
+ if (mDrawableContainerState.mEnterFadeDuration > 0) {
+ mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;
+ }
+ initializeDrawableForDisplay(d);
+ }
+ } else {
+ mCurrDrawable = null;
+ mCurIndex = -1;
+ }
+
+ if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
+ if (mAnimationRunnable == null) {
+ mAnimationRunnable = new Runnable() {
+ @Override public void run() {
+ animate(true);
+ invalidateSelf();
+ }
+ };
+ } else {
+ unscheduleSelf(mAnimationRunnable);
+ }
+ // Compute first frame and schedule next animation.
+ animate(true);
+ }
+
+ invalidateSelf();
+
+ return true;
+ }
+
+ /**
+ * Initializes a drawable for display in this container.
+ *
+ * @param d The drawable to initialize.
+ */
+ private void initializeDrawableForDisplay(Drawable d) {
+ if (mBlockInvalidateCallback == null) {
+ mBlockInvalidateCallback = new BlockInvalidateCallback();
+ }
+
+ // Temporary fix for suspending callbacks during initialization. We
+ // don't want any of these setters causing an invalidate() since that
+ // may call back into DrawableContainer.
+ d.setCallback(mBlockInvalidateCallback.wrap(d.getCallback()));
+
+ try {
+ if (mDrawableContainerState.mEnterFadeDuration <= 0 && mHasAlpha) {
+ d.setAlpha(mAlpha);
+ }
+
+ if (mDrawableContainerState.mHasColorFilter) {
+ // Color filter always overrides tint.
+ d.setColorFilter(mDrawableContainerState.mColorFilter);
+ } else {
+ if (mDrawableContainerState.mHasTintList) {
+ d.setTintList(mDrawableContainerState.mTintList);
+ }
+ if (mDrawableContainerState.mHasTintMode) {
+ d.setTintMode(mDrawableContainerState.mTintMode);
+ }
+ }
+
+ d.setVisible(isVisible(), true);
+ d.setDither(mDrawableContainerState.mDither);
+ d.setState(getState());
+ d.setLevel(getLevel());
+ d.setBounds(getBounds());
+ d.setLayoutDirection(getLayoutDirection());
+ d.setAutoMirrored(mDrawableContainerState.mAutoMirrored);
+
+ final Rect hotspotBounds = mHotspotBounds;
+ if (hotspotBounds != null) {
+ d.setHotspotBounds(hotspotBounds.left, hotspotBounds.top,
+ hotspotBounds.right, hotspotBounds.bottom);
+ }
+ } finally {
+ d.setCallback(mBlockInvalidateCallback.unwrap());
+ }
+ }
+
+ void animate(boolean schedule) {
+ mHasAlpha = true;
+
+ final long now = SystemClock.uptimeMillis();
+ boolean animating = false;
+ if (mCurrDrawable != null) {
+ if (mEnterAnimationEnd != 0) {
+ if (mEnterAnimationEnd <= now) {
+ mCurrDrawable.setAlpha(mAlpha);
+ mEnterAnimationEnd = 0;
+ } else {
+ int animAlpha = (int)((mEnterAnimationEnd-now)*255)
+ / mDrawableContainerState.mEnterFadeDuration;
+ mCurrDrawable.setAlpha(((255-animAlpha)*mAlpha)/255);
+ animating = true;
+ }
+ }
+ } else {
+ mEnterAnimationEnd = 0;
+ }
+ if (mLastDrawable != null) {
+ if (mExitAnimationEnd != 0) {
+ if (mExitAnimationEnd <= now) {
+ mLastDrawable.setVisible(false, false);
+ mLastDrawable = null;
+ mLastIndex = -1;
+ mExitAnimationEnd = 0;
+ } else {
+ int animAlpha = (int)((mExitAnimationEnd-now)*255)
+ / mDrawableContainerState.mExitFadeDuration;
+ mLastDrawable.setAlpha((animAlpha*mAlpha)/255);
+ animating = true;
+ }
+ }
+ } else {
+ mExitAnimationEnd = 0;
+ }
+
+ if (schedule && animating) {
+ scheduleSelf(mAnimationRunnable, now + 1000 / 60);
+ }
+ }
+
+ @Override
+ public Drawable getCurrent() {
+ return mCurrDrawable;
+ }
+
+ /**
+ * Updates the source density based on the resources used to inflate
+ * density-dependent values. Implementing classes should call this method
+ * during inflation.
+ *
+ * @param res the resources used to inflate density-dependent values
+ * @hide
+ */
+ protected final void updateDensity(Resources res) {
+ mDrawableContainerState.updateDensity(res);
+ }
+
+ @Override
+ public void applyTheme(Theme theme) {
+ mDrawableContainerState.applyTheme(theme);
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mDrawableContainerState.canApplyTheme();
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ if (mDrawableContainerState.canConstantState()) {
+ mDrawableContainerState.mChangingConfigurations = getChangingConfigurations();
+ return mDrawableContainerState;
+ }
+ return null;
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ final DrawableContainerState clone = cloneConstantState();
+ clone.mutate();
+ setConstantState(clone);
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * Returns a shallow copy of the container's constant state to be used as
+ * the base state for {@link #mutate()}.
+ *
+ * @return a shallow copy of the constant state
+ */
+ DrawableContainerState cloneConstantState() {
+ return mDrawableContainerState;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mDrawableContainerState.clearMutated();
+ mMutated = false;
+ }
+
+ /**
+ * A ConstantState that can contain several {@link Drawable}s.
+ *
+ * This class was made public to enable testing, and its visibility may change in a future
+ * release.
+ */
+ public abstract static class DrawableContainerState extends ConstantState {
+ final DrawableContainer mOwner;
+
+ Resources mSourceRes;
+ int mDensity = DisplayMetrics.DENSITY_DEFAULT;
+ @Config int mChangingConfigurations;
+ @Config int mChildrenChangingConfigurations;
+
+ SparseArray<ConstantState> mDrawableFutures;
+ Drawable[] mDrawables;
+ int mNumChildren;
+
+ boolean mVariablePadding = false;
+ boolean mCheckedPadding;
+ Rect mConstantPadding;
+
+ boolean mConstantSize = false;
+ boolean mCheckedConstantSize;
+ int mConstantWidth;
+ int mConstantHeight;
+ int mConstantMinimumWidth;
+ int mConstantMinimumHeight;
+
+ boolean mCheckedOpacity;
+ int mOpacity;
+
+ boolean mCheckedStateful;
+ boolean mStateful;
+
+ boolean mCheckedConstantState;
+ boolean mCanConstantState;
+
+ boolean mDither = DEFAULT_DITHER;
+
+ boolean mMutated;
+ int mLayoutDirection;
+
+ int mEnterFadeDuration = 0;
+ int mExitFadeDuration = 0;
+
+ boolean mAutoMirrored;
+
+ ColorFilter mColorFilter;
+ boolean mHasColorFilter;
+
+ ColorStateList mTintList;
+ Mode mTintMode;
+ boolean mHasTintList;
+ boolean mHasTintMode;
+
+ /**
+ * @hide
+ */
+ protected DrawableContainerState(DrawableContainerState orig, DrawableContainer owner,
+ Resources res) {
+ mOwner = owner;
+ mSourceRes = res != null ? res : (orig != null ? orig.mSourceRes : null);
+ mDensity = Drawable.resolveDensity(res, orig != null ? orig.mDensity : 0);
+
+ if (orig != null) {
+ mChangingConfigurations = orig.mChangingConfigurations;
+ mChildrenChangingConfigurations = orig.mChildrenChangingConfigurations;
+
+ mCheckedConstantState = true;
+ mCanConstantState = true;
+
+ mVariablePadding = orig.mVariablePadding;
+ mConstantSize = orig.mConstantSize;
+ mDither = orig.mDither;
+ mMutated = orig.mMutated;
+ mLayoutDirection = orig.mLayoutDirection;
+ mEnterFadeDuration = orig.mEnterFadeDuration;
+ mExitFadeDuration = orig.mExitFadeDuration;
+ mAutoMirrored = orig.mAutoMirrored;
+ mColorFilter = orig.mColorFilter;
+ mHasColorFilter = orig.mHasColorFilter;
+ mTintList = orig.mTintList;
+ mTintMode = orig.mTintMode;
+ mHasTintList = orig.mHasTintList;
+ mHasTintMode = orig.mHasTintMode;
+
+ if (orig.mDensity == mDensity) {
+ if (orig.mCheckedPadding) {
+ mConstantPadding = new Rect(orig.mConstantPadding);
+ mCheckedPadding = true;
+ }
+
+ if (orig.mCheckedConstantSize) {
+ mConstantWidth = orig.mConstantWidth;
+ mConstantHeight = orig.mConstantHeight;
+ mConstantMinimumWidth = orig.mConstantMinimumWidth;
+ mConstantMinimumHeight = orig.mConstantMinimumHeight;
+ mCheckedConstantSize = true;
+ }
+ }
+
+ if (orig.mCheckedOpacity) {
+ mOpacity = orig.mOpacity;
+ mCheckedOpacity = true;
+ }
+
+ if (orig.mCheckedStateful) {
+ mStateful = orig.mStateful;
+ mCheckedStateful = true;
+ }
+
+ // Postpone cloning children and futures until we're absolutely
+ // sure that we're done computing values for the original state.
+ final Drawable[] origDr = orig.mDrawables;
+ mDrawables = new Drawable[origDr.length];
+ mNumChildren = orig.mNumChildren;
+
+ final SparseArray<ConstantState> origDf = orig.mDrawableFutures;
+ if (origDf != null) {
+ mDrawableFutures = origDf.clone();
+ } else {
+ mDrawableFutures = new SparseArray<>(mNumChildren);
+ }
+
+ // Create futures for drawables with constant states. If a
+ // drawable doesn't have a constant state, then we can't clone
+ // it and we'll have to reference the original.
+ final int N = mNumChildren;
+ for (int i = 0; i < N; i++) {
+ if (origDr[i] != null) {
+ final ConstantState cs = origDr[i].getConstantState();
+ if (cs != null) {
+ mDrawableFutures.put(i, cs);
+ } else {
+ mDrawables[i] = origDr[i];
+ }
+ }
+ }
+ } else {
+ mDrawables = new Drawable[10];
+ mNumChildren = 0;
+ }
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations | mChildrenChangingConfigurations;
+ }
+
+ /**
+ * Adds the drawable to the end of the list of contained drawables.
+ *
+ * @param dr the drawable to add
+ * @return the position of the drawable within the container
+ */
+ public final int addChild(Drawable dr) {
+ final int pos = mNumChildren;
+ if (pos >= mDrawables.length) {
+ growArray(pos, pos+10);
+ }
+
+ dr.mutate();
+ dr.setVisible(false, true);
+ dr.setCallback(mOwner);
+
+ mDrawables[pos] = dr;
+ mNumChildren++;
+ mChildrenChangingConfigurations |= dr.getChangingConfigurations();
+
+ invalidateCache();
+
+ mConstantPadding = null;
+ mCheckedPadding = false;
+ mCheckedConstantSize = false;
+ mCheckedConstantState = false;
+
+ return pos;
+ }
+
+ /**
+ * Invalidates the cached opacity and statefulness.
+ */
+ void invalidateCache() {
+ mCheckedOpacity = false;
+ mCheckedStateful = false;
+ }
+
+ final int getCapacity() {
+ return mDrawables.length;
+ }
+
+ private void createAllFutures() {
+ if (mDrawableFutures != null) {
+ final int futureCount = mDrawableFutures.size();
+ for (int keyIndex = 0; keyIndex < futureCount; keyIndex++) {
+ final int index = mDrawableFutures.keyAt(keyIndex);
+ final ConstantState cs = mDrawableFutures.valueAt(keyIndex);
+ mDrawables[index] = prepareDrawable(cs.newDrawable(mSourceRes));
+ }
+
+ mDrawableFutures = null;
+ }
+ }
+
+ private Drawable prepareDrawable(Drawable child) {
+ child.setLayoutDirection(mLayoutDirection);
+ child = child.mutate();
+ child.setCallback(mOwner);
+ return child;
+ }
+
+ public final int getChildCount() {
+ return mNumChildren;
+ }
+
+ /*
+ * @deprecated Use {@link #getChild} instead.
+ */
+ public final Drawable[] getChildren() {
+ // Create all futures for backwards compatibility.
+ createAllFutures();
+
+ return mDrawables;
+ }
+
+ public final Drawable getChild(int index) {
+ final Drawable result = mDrawables[index];
+ if (result != null) {
+ return result;
+ }
+
+ // Prepare future drawable if necessary.
+ if (mDrawableFutures != null) {
+ final int keyIndex = mDrawableFutures.indexOfKey(index);
+ if (keyIndex >= 0) {
+ final ConstantState cs = mDrawableFutures.valueAt(keyIndex);
+ final Drawable prepared = prepareDrawable(cs.newDrawable(mSourceRes));
+ mDrawables[index] = prepared;
+ mDrawableFutures.removeAt(keyIndex);
+ if (mDrawableFutures.size() == 0) {
+ mDrawableFutures = null;
+ }
+ return prepared;
+ }
+ }
+
+ return null;
+ }
+
+ final boolean setLayoutDirection(int layoutDirection, int currentIndex) {
+ boolean changed = false;
+
+ // No need to call createAllFutures, since future drawables will
+ // change layout direction when they are prepared.
+ final int N = mNumChildren;
+ final Drawable[] drawables = mDrawables;
+ for (int i = 0; i < N; i++) {
+ if (drawables[i] != null) {
+ final boolean childChanged = drawables[i].setLayoutDirection(layoutDirection);
+ if (i == currentIndex) {
+ changed = childChanged;
+ }
+ }
+ }
+
+ mLayoutDirection = layoutDirection;
+
+ return changed;
+ }
+
+ /**
+ * Updates the source density based on the resources used to inflate
+ * density-dependent values.
+ *
+ * @param res the resources used to inflate density-dependent values
+ */
+ final void updateDensity(Resources res) {
+ if (res != null) {
+ mSourceRes = res;
+
+ // The density may have changed since the last update (if any). Any
+ // dimension-type attributes will need their default values scaled.
+ final int targetDensity = Drawable.resolveDensity(res, mDensity);
+ final int sourceDensity = mDensity;
+ mDensity = targetDensity;
+
+ if (sourceDensity != targetDensity) {
+ mCheckedConstantSize = false;
+ mCheckedPadding = false;
+ }
+ }
+ }
+
+ final void applyTheme(Theme theme) {
+ if (theme != null) {
+ createAllFutures();
+
+ final int N = mNumChildren;
+ final Drawable[] drawables = mDrawables;
+ for (int i = 0; i < N; i++) {
+ if (drawables[i] != null && drawables[i].canApplyTheme()) {
+ drawables[i].applyTheme(theme);
+
+ // Update cached mask of child changing configurations.
+ mChildrenChangingConfigurations |= drawables[i].getChangingConfigurations();
+ }
+ }
+
+ updateDensity(theme.getResources());
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ final int N = mNumChildren;
+ final Drawable[] drawables = mDrawables;
+ for (int i = 0; i < N; i++) {
+ final Drawable d = drawables[i];
+ if (d != null) {
+ if (d.canApplyTheme()) {
+ return true;
+ }
+ } else {
+ final ConstantState future = mDrawableFutures.get(i);
+ if (future != null && future.canApplyTheme()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private void mutate() {
+ // No need to call createAllFutures, since future drawables will
+ // mutate when they are prepared.
+ final int N = mNumChildren;
+ final Drawable[] drawables = mDrawables;
+ for (int i = 0; i < N; i++) {
+ if (drawables[i] != null) {
+ drawables[i].mutate();
+ }
+ }
+
+ mMutated = true;
+ }
+
+ final void clearMutated() {
+ final int N = mNumChildren;
+ final Drawable[] drawables = mDrawables;
+ for (int i = 0; i < N; i++) {
+ if (drawables[i] != null) {
+ drawables[i].clearMutated();
+ }
+ }
+
+ mMutated = false;
+ }
+
+ /**
+ * A boolean value indicating whether to use the maximum padding value
+ * of all frames in the set (false), or to use the padding value of the
+ * frame being shown (true). Default value is false.
+ */
+ public final void setVariablePadding(boolean variable) {
+ mVariablePadding = variable;
+ }
+
+ public final Rect getConstantPadding() {
+ if (mVariablePadding) {
+ return null;
+ }
+
+ if ((mConstantPadding != null) || mCheckedPadding) {
+ return mConstantPadding;
+ }
+
+ createAllFutures();
+
+ Rect r = null;
+ final Rect t = new Rect();
+ final int N = mNumChildren;
+ final Drawable[] drawables = mDrawables;
+ for (int i = 0; i < N; i++) {
+ if (drawables[i].getPadding(t)) {
+ if (r == null) r = new Rect(0, 0, 0, 0);
+ if (t.left > r.left) r.left = t.left;
+ if (t.top > r.top) r.top = t.top;
+ if (t.right > r.right) r.right = t.right;
+ if (t.bottom > r.bottom) r.bottom = t.bottom;
+ }
+ }
+
+ mCheckedPadding = true;
+ return (mConstantPadding = r);
+ }
+
+ public final void setConstantSize(boolean constant) {
+ mConstantSize = constant;
+ }
+
+ public final boolean isConstantSize() {
+ return mConstantSize;
+ }
+
+ public final int getConstantWidth() {
+ if (!mCheckedConstantSize) {
+ computeConstantSize();
+ }
+
+ return mConstantWidth;
+ }
+
+ public final int getConstantHeight() {
+ if (!mCheckedConstantSize) {
+ computeConstantSize();
+ }
+
+ return mConstantHeight;
+ }
+
+ public final int getConstantMinimumWidth() {
+ if (!mCheckedConstantSize) {
+ computeConstantSize();
+ }
+
+ return mConstantMinimumWidth;
+ }
+
+ public final int getConstantMinimumHeight() {
+ if (!mCheckedConstantSize) {
+ computeConstantSize();
+ }
+
+ return mConstantMinimumHeight;
+ }
+
+ protected void computeConstantSize() {
+ mCheckedConstantSize = true;
+
+ createAllFutures();
+
+ final int N = mNumChildren;
+ final Drawable[] drawables = mDrawables;
+ mConstantWidth = mConstantHeight = -1;
+ mConstantMinimumWidth = mConstantMinimumHeight = 0;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = drawables[i];
+ int s = dr.getIntrinsicWidth();
+ if (s > mConstantWidth) mConstantWidth = s;
+ s = dr.getIntrinsicHeight();
+ if (s > mConstantHeight) mConstantHeight = s;
+ s = dr.getMinimumWidth();
+ if (s > mConstantMinimumWidth) mConstantMinimumWidth = s;
+ s = dr.getMinimumHeight();
+ if (s > mConstantMinimumHeight) mConstantMinimumHeight = s;
+ }
+ }
+
+ public final void setEnterFadeDuration(int duration) {
+ mEnterFadeDuration = duration;
+ }
+
+ public final int getEnterFadeDuration() {
+ return mEnterFadeDuration;
+ }
+
+ public final void setExitFadeDuration(int duration) {
+ mExitFadeDuration = duration;
+ }
+
+ public final int getExitFadeDuration() {
+ return mExitFadeDuration;
+ }
+
+ public final int getOpacity() {
+ if (mCheckedOpacity) {
+ return mOpacity;
+ }
+
+ createAllFutures();
+
+ final int N = mNumChildren;
+ final Drawable[] drawables = mDrawables;
+ int op = (N > 0) ? drawables[0].getOpacity() : PixelFormat.TRANSPARENT;
+ for (int i = 1; i < N; i++) {
+ op = Drawable.resolveOpacity(op, drawables[i].getOpacity());
+ }
+
+ mOpacity = op;
+ mCheckedOpacity = true;
+ return op;
+ }
+
+ public final boolean isStateful() {
+ if (mCheckedStateful) {
+ return mStateful;
+ }
+
+ createAllFutures();
+
+ final int N = mNumChildren;
+ final Drawable[] drawables = mDrawables;
+ boolean isStateful = false;
+ for (int i = 0; i < N; i++) {
+ if (drawables[i].isStateful()) {
+ isStateful = true;
+ break;
+ }
+ }
+
+ mStateful = isStateful;
+ mCheckedStateful = true;
+ return isStateful;
+ }
+
+ public void growArray(int oldSize, int newSize) {
+ Drawable[] newDrawables = new Drawable[newSize];
+ System.arraycopy(mDrawables, 0, newDrawables, 0, oldSize);
+ mDrawables = newDrawables;
+ }
+
+ public synchronized boolean canConstantState() {
+ if (mCheckedConstantState) {
+ return mCanConstantState;
+ }
+
+ createAllFutures();
+
+ mCheckedConstantState = true;
+
+ final int N = mNumChildren;
+ final Drawable[] drawables = mDrawables;
+ for (int i = 0; i < N; i++) {
+ if (drawables[i].getConstantState() == null) {
+ mCanConstantState = false;
+ return false;
+ }
+ }
+
+ mCanConstantState = true;
+ return true;
+ }
+
+ }
+
+ protected void setConstantState(DrawableContainerState state) {
+ mDrawableContainerState = state;
+
+ // The locally cached drawables may have changed.
+ if (mCurIndex >= 0) {
+ mCurrDrawable = state.getChild(mCurIndex);
+ if (mCurrDrawable != null) {
+ initializeDrawableForDisplay(mCurrDrawable);
+ }
+ }
+
+ // Clear out the last drawable. We don't have enough information to
+ // propagate local state from the past.
+ mLastIndex = -1;
+ mLastDrawable = null;
+ }
+
+ /**
+ * Callback that blocks drawable invalidation.
+ */
+ private static class BlockInvalidateCallback implements Drawable.Callback {
+ private Drawable.Callback mCallback;
+
+ public BlockInvalidateCallback wrap(Drawable.Callback callback) {
+ mCallback = callback;
+ return this;
+ }
+
+ public Drawable.Callback unwrap() {
+ final Drawable.Callback callback = mCallback;
+ mCallback = null;
+ return callback;
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable who) {
+ // Ignore invalidation.
+ }
+
+ @Override
+ public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+ if (mCallback != null) {
+ mCallback.scheduleDrawable(who, what, when);
+ }
+ }
+
+ @Override
+ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+ if (mCallback != null) {
+ mCallback.unscheduleDrawable(who, what);
+ }
+ }
+ }
+}
diff --git a/android/graphics/drawable/DrawableInflater.java b/android/graphics/drawable/DrawableInflater.java
new file mode 100644
index 0000000..eea7048
--- /dev/null
+++ b/android/graphics/drawable/DrawableInflater.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2015 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 android.graphics.drawable;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.util.AttributeSet;
+import android.view.InflateException;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+
+/**
+ * Instantiates a drawable XML file into its corresponding
+ * {@link android.graphics.drawable.Drawable} objects.
+ * <p>
+ * For performance reasons, inflation relies heavily on pre-processing of
+ * XML files that is done at build time. Therefore, it is not currently possible
+ * to use this inflater with an XmlPullParser over a plain XML file at runtime;
+ * it only works with an XmlPullParser returned from a compiled resource (R.
+ * <em>something</em> file.)
+ *
+ * @hide Pending API finalization.
+ */
+public final class DrawableInflater {
+ private static final HashMap<String, Constructor<? extends Drawable>> CONSTRUCTOR_MAP =
+ new HashMap<>();
+
+ private final Resources mRes;
+ private final ClassLoader mClassLoader;
+
+ /**
+ * Loads the drawable resource with the specified identifier.
+ *
+ * @param context the context in which the drawable should be loaded
+ * @param id the identifier of the drawable resource
+ * @return a drawable, or {@code null} if the drawable failed to load
+ */
+ @Nullable
+ public static Drawable loadDrawable(@NonNull Context context, @DrawableRes int id) {
+ return loadDrawable(context.getResources(), context.getTheme(), id);
+ }
+
+ /**
+ * Loads the drawable resource with the specified identifier.
+ *
+ * @param resources the resources from which the drawable should be loaded
+ * @param theme the theme against which the drawable should be inflated
+ * @param id the identifier of the drawable resource
+ * @return a drawable, or {@code null} if the drawable failed to load
+ */
+ @Nullable
+ public static Drawable loadDrawable(
+ @NonNull Resources resources, @Nullable Theme theme, @DrawableRes int id) {
+ return resources.getDrawable(id, theme);
+ }
+
+ /**
+ * Constructs a new drawable inflater using the specified resources and
+ * class loader.
+ *
+ * @param res the resources used to resolve resource identifiers
+ * @param classLoader the class loader used to load custom drawables
+ * @hide
+ */
+ public DrawableInflater(@NonNull Resources res, @NonNull ClassLoader classLoader) {
+ mRes = res;
+ mClassLoader = classLoader;
+ }
+
+ /**
+ * Inflates a drawable from inside an XML document using an optional
+ * {@link Theme}.
+ * <p>
+ * This method should be called on a parser positioned at a tag in an XML
+ * document defining a drawable resource. It will attempt to create a
+ * Drawable from the tag at the current position.
+ *
+ * @param name the name of the tag at the current position
+ * @param parser an XML parser positioned at the drawable tag
+ * @param attrs an attribute set that wraps the parser
+ * @param theme the theme against which the drawable should be inflated, or
+ * {@code null} to not inflate against a theme
+ * @return a drawable
+ *
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ @NonNull
+ public Drawable inflateFromXml(@NonNull String name, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ return inflateFromXmlForDensity(name, parser, attrs, 0, theme);
+ }
+
+ /**
+ * Version of {@link #inflateFromXml(String, XmlPullParser, AttributeSet, Theme)} that accepts
+ * an override density.
+ */
+ @NonNull
+ Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, int density, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ // Inner classes must be referenced as Outer$Inner, but XML tag names
+ // can't contain $, so the <drawable> tag allows developers to specify
+ // the class in an attribute. We'll still run it through inflateFromTag
+ // to stay consistent with how LayoutInflater works.
+ if (name.equals("drawable")) {
+ name = attrs.getAttributeValue(null, "class");
+ if (name == null) {
+ throw new InflateException("<drawable> tag must specify class attribute");
+ }
+ }
+
+ Drawable drawable = inflateFromTag(name);
+ if (drawable == null) {
+ drawable = inflateFromClass(name);
+ }
+ drawable.setSrcDensityOverride(density);
+ drawable.inflate(mRes, parser, attrs, theme);
+ return drawable;
+ }
+
+ @NonNull
+ @SuppressWarnings("deprecation")
+ private Drawable inflateFromTag(@NonNull String name) {
+ switch (name) {
+ case "selector":
+ return new StateListDrawable();
+ case "animated-selector":
+ return new AnimatedStateListDrawable();
+ case "level-list":
+ return new LevelListDrawable();
+ case "layer-list":
+ return new LayerDrawable();
+ case "transition":
+ return new TransitionDrawable();
+ case "ripple":
+ return new RippleDrawable();
+ case "adaptive-icon":
+ return new AdaptiveIconDrawable();
+ case "color":
+ return new ColorDrawable();
+ case "shape":
+ return new GradientDrawable();
+ case "vector":
+ return new VectorDrawable();
+ case "animated-vector":
+ return new AnimatedVectorDrawable();
+ case "scale":
+ return new ScaleDrawable();
+ case "clip":
+ return new ClipDrawable();
+ case "rotate":
+ return new RotateDrawable();
+ case "animated-rotate":
+ return new AnimatedRotateDrawable();
+ case "animation-list":
+ return new AnimationDrawable();
+ case "inset":
+ return new InsetDrawable();
+ case "bitmap":
+ return new BitmapDrawable();
+ case "nine-patch":
+ return new NinePatchDrawable();
+ default:
+ return null;
+ }
+ }
+
+ @NonNull
+ private Drawable inflateFromClass(@NonNull String className) {
+ try {
+ Constructor<? extends Drawable> constructor;
+ synchronized (CONSTRUCTOR_MAP) {
+ constructor = CONSTRUCTOR_MAP.get(className);
+ if (constructor == null) {
+ final Class<? extends Drawable> clazz =
+ mClassLoader.loadClass(className).asSubclass(Drawable.class);
+ constructor = clazz.getConstructor();
+ CONSTRUCTOR_MAP.put(className, constructor);
+ }
+ }
+ return constructor.newInstance();
+ } catch (NoSuchMethodException e) {
+ final InflateException ie = new InflateException(
+ "Error inflating class " + className);
+ ie.initCause(e);
+ throw ie;
+ } catch (ClassCastException e) {
+ // If loaded class is not a Drawable subclass.
+ final InflateException ie = new InflateException(
+ "Class is not a Drawable " + className);
+ ie.initCause(e);
+ throw ie;
+ } catch (ClassNotFoundException e) {
+ // If loadClass fails, we should propagate the exception.
+ final InflateException ie = new InflateException(
+ "Class not found " + className);
+ ie.initCause(e);
+ throw ie;
+ } catch (Exception e) {
+ final InflateException ie = new InflateException(
+ "Error inflating class " + className);
+ ie.initCause(e);
+ throw ie;
+ }
+ }
+}
diff --git a/android/graphics/drawable/DrawableWrapper.java b/android/graphics/drawable/DrawableWrapper.java
new file mode 100644
index 0000000..cf821bb
--- /dev/null
+++ b/android/graphics/drawable/DrawableWrapper.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2015 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 android.graphics.drawable;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Insets;
+import android.graphics.Outline;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.View;
+
+import java.io.IOException;
+
+/**
+ * Drawable container with only one child element.
+ */
+public abstract class DrawableWrapper extends Drawable implements Drawable.Callback {
+ private DrawableWrapperState mState;
+ private Drawable mDrawable;
+ private boolean mMutated;
+
+ DrawableWrapper(DrawableWrapperState state, Resources res) {
+ mState = state;
+
+ updateLocalState(res);
+ }
+
+ /**
+ * Creates a new wrapper around the specified drawable.
+ *
+ * @param dr the drawable to wrap
+ */
+ public DrawableWrapper(@Nullable Drawable dr) {
+ mState = null;
+ mDrawable = dr;
+ }
+
+ /**
+ * Initializes local dynamic properties from state. This should be called
+ * after significant state changes, e.g. from the One True Constructor and
+ * after inflating or applying a theme.
+ */
+ private void updateLocalState(Resources res) {
+ if (mState != null && mState.mDrawableState != null) {
+ final Drawable dr = mState.mDrawableState.newDrawable(res);
+ setDrawable(dr);
+ }
+ }
+
+ /**
+ * Sets the wrapped drawable.
+ *
+ * @param dr the wrapped drawable
+ */
+ public void setDrawable(@Nullable Drawable dr) {
+ if (mDrawable != null) {
+ mDrawable.setCallback(null);
+ }
+
+ mDrawable = dr;
+
+ if (dr != null) {
+ dr.setCallback(this);
+
+ // Only call setters for data that's stored in the base Drawable.
+ dr.setVisible(isVisible(), true);
+ dr.setState(getState());
+ dr.setLevel(getLevel());
+ dr.setBounds(getBounds());
+ dr.setLayoutDirection(getLayoutDirection());
+
+ if (mState != null) {
+ mState.mDrawableState = dr.getConstantState();
+ }
+ }
+
+ invalidateSelf();
+ }
+
+ /**
+ * @return the wrapped drawable
+ */
+ @Nullable
+ public Drawable getDrawable() {
+ return mDrawable;
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ super.inflate(r, parser, attrs, theme);
+
+ final DrawableWrapperState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ // The density may have changed since the last update. This will
+ // apply scaling to any existing constant state properties.
+ final int densityDpi = r.getDisplayMetrics().densityDpi;
+ final int targetDensity = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
+ state.setDensity(targetDensity);
+ state.mSrcDensityOverride = mSrcDensityOverride;
+
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.DrawableWrapper);
+ updateStateFromTypedArray(a);
+ a.recycle();
+
+ inflateChildDrawable(r, parser, attrs, theme);
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ // If we load the drawable later as part of updating from the typed
+ // array, it will already be themed correctly. So, we can theme the
+ // local drawable first.
+ if (mDrawable != null && mDrawable.canApplyTheme()) {
+ mDrawable.applyTheme(t);
+ }
+
+ final DrawableWrapperState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ final int densityDpi = t.getResources().getDisplayMetrics().densityDpi;
+ final int density = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
+ state.setDensity(density);
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(
+ state.mThemeAttrs, R.styleable.DrawableWrapper);
+ updateStateFromTypedArray(a);
+ a.recycle();
+ }
+ }
+
+ /**
+ * Updates constant state properties from the provided typed array.
+ * <p>
+ * Implementing subclasses should call through to the super method first.
+ *
+ * @param a the typed array rom which properties should be read
+ */
+ private void updateStateFromTypedArray(@NonNull TypedArray a) {
+ final DrawableWrapperState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ if (a.hasValueOrEmpty(R.styleable.DrawableWrapper_drawable)) {
+ setDrawable(a.getDrawable(R.styleable.DrawableWrapper_drawable));
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable who) {
+ final Callback callback = getCallback();
+ if (callback != null) {
+ callback.invalidateDrawable(this);
+ }
+ }
+
+ @Override
+ public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+ final Callback callback = getCallback();
+ if (callback != null) {
+ callback.scheduleDrawable(this, what, when);
+ }
+ }
+
+ @Override
+ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+ final Callback callback = getCallback();
+ if (callback != null) {
+ callback.unscheduleDrawable(this, what);
+ }
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ if (mDrawable != null) {
+ mDrawable.draw(canvas);
+ }
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations()
+ | (mState != null ? mState.getChangingConfigurations() : 0)
+ | mDrawable.getChangingConfigurations();
+ }
+
+ @Override
+ public boolean getPadding(@NonNull Rect padding) {
+ return mDrawable != null && mDrawable.getPadding(padding);
+ }
+
+ /** @hide */
+ @Override
+ public Insets getOpticalInsets() {
+ return mDrawable != null ? mDrawable.getOpticalInsets() : Insets.NONE;
+ }
+
+ @Override
+ public void setHotspot(float x, float y) {
+ if (mDrawable != null) {
+ mDrawable.setHotspot(x, y);
+ }
+ }
+
+ @Override
+ public void setHotspotBounds(int left, int top, int right, int bottom) {
+ if (mDrawable != null) {
+ mDrawable.setHotspotBounds(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public void getHotspotBounds(@NonNull Rect outRect) {
+ if (mDrawable != null) {
+ mDrawable.getHotspotBounds(outRect);
+ } else {
+ outRect.set(getBounds());
+ }
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ final boolean superChanged = super.setVisible(visible, restart);
+ final boolean changed = mDrawable != null && mDrawable.setVisible(visible, restart);
+ return superChanged | changed;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (mDrawable != null) {
+ mDrawable.setAlpha(alpha);
+ }
+ }
+
+ @Override
+ public int getAlpha() {
+ return mDrawable != null ? mDrawable.getAlpha() : 255;
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ if (mDrawable != null) {
+ mDrawable.setColorFilter(colorFilter);
+ }
+ }
+
+ @Override
+ public void setTintList(@Nullable ColorStateList tint) {
+ if (mDrawable != null) {
+ mDrawable.setTintList(tint);
+ }
+ }
+
+ @Override
+ public void setTintMode(@Nullable PorterDuff.Mode tintMode) {
+ if (mDrawable != null) {
+ mDrawable.setTintMode(tintMode);
+ }
+ }
+
+ @Override
+ public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
+ return mDrawable != null && mDrawable.setLayoutDirection(layoutDirection);
+ }
+
+ @Override
+ public int getOpacity() {
+ return mDrawable != null ? mDrawable.getOpacity() : PixelFormat.TRANSPARENT;
+ }
+
+ @Override
+ public boolean isStateful() {
+ return mDrawable != null && mDrawable.isStateful();
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return mDrawable != null && mDrawable.hasFocusStateSpecified();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ if (mDrawable != null && mDrawable.isStateful()) {
+ final boolean changed = mDrawable.setState(state);
+ if (changed) {
+ onBoundsChange(getBounds());
+ }
+ return changed;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ return mDrawable != null && mDrawable.setLevel(level);
+ }
+
+ @Override
+ protected void onBoundsChange(@NonNull Rect bounds) {
+ if (mDrawable != null) {
+ mDrawable.setBounds(bounds);
+ }
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mDrawable != null ? mDrawable.getIntrinsicWidth() : -1;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mDrawable != null ? mDrawable.getIntrinsicHeight() : -1;
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ if (mDrawable != null) {
+ mDrawable.getOutline(outline);
+ } else {
+ super.getOutline(outline);
+ }
+ }
+
+ @Override
+ @Nullable
+ public ConstantState getConstantState() {
+ if (mState != null && mState.canConstantState()) {
+ mState.mChangingConfigurations = getChangingConfigurations();
+ return mState;
+ }
+ return null;
+ }
+
+ @Override
+ @NonNull
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mState = mutateConstantState();
+ if (mDrawable != null) {
+ mDrawable.mutate();
+ }
+ if (mState != null) {
+ mState.mDrawableState = mDrawable != null ? mDrawable.getConstantState() : null;
+ }
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * Mutates the constant state and returns the new state. Responsible for
+ * updating any local copy.
+ * <p>
+ * This method should never call the super implementation; it should always
+ * mutate and return its own constant state.
+ *
+ * @return the new state
+ */
+ DrawableWrapperState mutateConstantState() {
+ return mState;
+ }
+
+ /**
+ * @hide Only used by the framework for pre-loading resources.
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ if (mDrawable != null) {
+ mDrawable.clearMutated();
+ }
+ mMutated = false;
+ }
+
+ /**
+ * Called during inflation to inflate the child element. The last valid
+ * child element will take precedence over any other child elements or
+ * explicit drawable attribute.
+ */
+ private void inflateChildDrawable(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ // Seek to the first child element.
+ Drawable dr = null;
+ int type;
+ final int outerDepth = parser.getDepth();
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.START_TAG) {
+ dr = Drawable.createFromXmlInnerForDensity(r, parser, attrs,
+ mState.mSrcDensityOverride, theme);
+ }
+ }
+
+ if (dr != null) {
+ setDrawable(dr);
+ }
+ }
+
+ abstract static class DrawableWrapperState extends Drawable.ConstantState {
+ private int[] mThemeAttrs;
+
+ @Config int mChangingConfigurations;
+ int mDensity = DisplayMetrics.DENSITY_DEFAULT;
+
+ /**
+ * The density to use when looking up resources from
+ * {@link Resources#getDrawableForDensity(int, int, Theme)}.
+ * A value of 0 means there is no override and the system density will be used.
+ * @hide
+ */
+ int mSrcDensityOverride = 0;
+
+ Drawable.ConstantState mDrawableState;
+
+ DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) {
+ if (orig != null) {
+ mThemeAttrs = orig.mThemeAttrs;
+ mChangingConfigurations = orig.mChangingConfigurations;
+ mDrawableState = orig.mDrawableState;
+ mSrcDensityOverride = orig.mSrcDensityOverride;
+ }
+
+ final int density;
+ if (res != null) {
+ density = res.getDisplayMetrics().densityDpi;
+ } else if (orig != null) {
+ density = orig.mDensity;
+ } else {
+ density = 0;
+ }
+
+ mDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
+ }
+
+ /**
+ * Sets the constant state density.
+ * <p>
+ * If the density has been previously set, dispatches the change to
+ * subclasses so that density-dependent properties may be scaled as
+ * necessary.
+ *
+ * @param targetDensity the new constant state density
+ */
+ public final void setDensity(int targetDensity) {
+ if (mDensity != targetDensity) {
+ final int sourceDensity = mDensity;
+ mDensity = targetDensity;
+
+ onDensityChanged(sourceDensity, targetDensity);
+ }
+ }
+
+ /**
+ * Called when the constant state density changes.
+ * <p>
+ * Subclasses with density-dependent constant state properties should
+ * override this method and scale their properties as necessary.
+ *
+ * @param sourceDensity the previous constant state density
+ * @param targetDensity the new constant state density
+ */
+ void onDensityChanged(int sourceDensity, int targetDensity) {
+ // Stub method.
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mThemeAttrs != null
+ || (mDrawableState != null && mDrawableState.canApplyTheme())
+ || super.canApplyTheme();
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return newDrawable(null);
+ }
+
+ @Override
+ public abstract Drawable newDrawable(@Nullable Resources res);
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations
+ | (mDrawableState != null ? mDrawableState.getChangingConfigurations() : 0);
+ }
+
+ public boolean canConstantState() {
+ return mDrawableState != null;
+ }
+ }
+}
diff --git a/android/graphics/drawable/GradientDrawable.java b/android/graphics/drawable/GradientDrawable.java
new file mode 100644
index 0000000..6c3aea2
--- /dev/null
+++ b/android/graphics/drawable/GradientDrawable.java
@@ -0,0 +1,2157 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import android.annotation.ColorInt;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.DashPathEffect;
+import android.graphics.Insets;
+import android.graphics.LinearGradient;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.SweepGradient;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A Drawable with a color gradient for buttons, backgrounds, etc.
+ *
+ * <p>It can be defined in an XML file with the <code><shape></code> element. For more
+ * information, see the guide to <a
+ * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
+ *
+ * @attr ref android.R.styleable#GradientDrawable_visible
+ * @attr ref android.R.styleable#GradientDrawable_shape
+ * @attr ref android.R.styleable#GradientDrawable_innerRadiusRatio
+ * @attr ref android.R.styleable#GradientDrawable_innerRadius
+ * @attr ref android.R.styleable#GradientDrawable_thicknessRatio
+ * @attr ref android.R.styleable#GradientDrawable_thickness
+ * @attr ref android.R.styleable#GradientDrawable_useLevel
+ * @attr ref android.R.styleable#GradientDrawableSize_width
+ * @attr ref android.R.styleable#GradientDrawableSize_height
+ * @attr ref android.R.styleable#GradientDrawableGradient_startColor
+ * @attr ref android.R.styleable#GradientDrawableGradient_centerColor
+ * @attr ref android.R.styleable#GradientDrawableGradient_endColor
+ * @attr ref android.R.styleable#GradientDrawableGradient_useLevel
+ * @attr ref android.R.styleable#GradientDrawableGradient_angle
+ * @attr ref android.R.styleable#GradientDrawableGradient_type
+ * @attr ref android.R.styleable#GradientDrawableGradient_centerX
+ * @attr ref android.R.styleable#GradientDrawableGradient_centerY
+ * @attr ref android.R.styleable#GradientDrawableGradient_gradientRadius
+ * @attr ref android.R.styleable#GradientDrawableSolid_color
+ * @attr ref android.R.styleable#GradientDrawableStroke_width
+ * @attr ref android.R.styleable#GradientDrawableStroke_color
+ * @attr ref android.R.styleable#GradientDrawableStroke_dashWidth
+ * @attr ref android.R.styleable#GradientDrawableStroke_dashGap
+ * @attr ref android.R.styleable#GradientDrawablePadding_left
+ * @attr ref android.R.styleable#GradientDrawablePadding_top
+ * @attr ref android.R.styleable#GradientDrawablePadding_right
+ * @attr ref android.R.styleable#GradientDrawablePadding_bottom
+ */
+public class GradientDrawable extends Drawable {
+ /**
+ * Shape is a rectangle, possibly with rounded corners
+ */
+ public static final int RECTANGLE = 0;
+
+ /**
+ * Shape is an ellipse
+ */
+ public static final int OVAL = 1;
+
+ /**
+ * Shape is a line
+ */
+ public static final int LINE = 2;
+
+ /**
+ * Shape is a ring.
+ */
+ public static final int RING = 3;
+
+ /** @hide */
+ @IntDef({RECTANGLE, OVAL, LINE, RING})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Shape {}
+
+ /**
+ * Gradient is linear (default.)
+ */
+ public static final int LINEAR_GRADIENT = 0;
+
+ /**
+ * Gradient is circular.
+ */
+ public static final int RADIAL_GRADIENT = 1;
+
+ /**
+ * Gradient is a sweep.
+ */
+ public static final int SWEEP_GRADIENT = 2;
+
+ /** @hide */
+ @IntDef({LINEAR_GRADIENT, RADIAL_GRADIENT, SWEEP_GRADIENT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface GradientType {}
+
+ /** Radius is in pixels. */
+ private static final int RADIUS_TYPE_PIXELS = 0;
+
+ /** Radius is a fraction of the base size. */
+ private static final int RADIUS_TYPE_FRACTION = 1;
+
+ /** Radius is a fraction of the bounds size. */
+ private static final int RADIUS_TYPE_FRACTION_PARENT = 2;
+
+ /** @hide */
+ @IntDef({RADIUS_TYPE_PIXELS, RADIUS_TYPE_FRACTION, RADIUS_TYPE_FRACTION_PARENT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RadiusType {}
+
+ private static final float DEFAULT_INNER_RADIUS_RATIO = 3.0f;
+ private static final float DEFAULT_THICKNESS_RATIO = 9.0f;
+
+ private GradientState mGradientState;
+
+ private final Paint mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private Rect mPadding;
+ private Paint mStrokePaint; // optional, set by the caller
+ private ColorFilter mColorFilter; // optional, set by the caller
+ private PorterDuffColorFilter mTintFilter;
+ private int mAlpha = 0xFF; // modified by the caller
+
+ private final Path mPath = new Path();
+ private final RectF mRect = new RectF();
+
+ private Paint mLayerPaint; // internal, used if we use saveLayer()
+ private boolean mGradientIsDirty;
+ private boolean mMutated;
+ private Path mRingPath;
+ private boolean mPathIsDirty = true;
+
+ /** Current gradient radius, valid when {@link #mGradientIsDirty} is false. */
+ private float mGradientRadius;
+
+ /**
+ * Controls how the gradient is oriented relative to the drawable's bounds
+ */
+ public enum Orientation {
+ /** draw the gradient from the top to the bottom */
+ TOP_BOTTOM,
+ /** draw the gradient from the top-right to the bottom-left */
+ TR_BL,
+ /** draw the gradient from the right to the left */
+ RIGHT_LEFT,
+ /** draw the gradient from the bottom-right to the top-left */
+ BR_TL,
+ /** draw the gradient from the bottom to the top */
+ BOTTOM_TOP,
+ /** draw the gradient from the bottom-left to the top-right */
+ BL_TR,
+ /** draw the gradient from the left to the right */
+ LEFT_RIGHT,
+ /** draw the gradient from the top-left to the bottom-right */
+ TL_BR,
+ }
+
+ public GradientDrawable() {
+ this(new GradientState(Orientation.TOP_BOTTOM, null), null);
+ }
+
+ /**
+ * Create a new gradient drawable given an orientation and an array
+ * of colors for the gradient.
+ */
+ public GradientDrawable(Orientation orientation, @ColorInt int[] colors) {
+ this(new GradientState(orientation, colors), null);
+ }
+
+ @Override
+ public boolean getPadding(Rect padding) {
+ if (mPadding != null) {
+ padding.set(mPadding);
+ return true;
+ } else {
+ return super.getPadding(padding);
+ }
+ }
+
+ /**
+ * Specifies radii for each of the 4 corners. For each corner, the array
+ * contains 2 values, <code>[X_radius, Y_radius]</code>. The corners are
+ * ordered top-left, top-right, bottom-right, bottom-left. This property
+ * is honored only when the shape is of type {@link #RECTANGLE}.
+ * <p>
+ * <strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.
+ *
+ * @param radii an array of length >= 8 containing 4 pairs of X and Y
+ * radius for each corner, specified in pixels
+ *
+ * @see #mutate()
+ * @see #setShape(int)
+ * @see #setCornerRadius(float)
+ */
+ public void setCornerRadii(@Nullable float[] radii) {
+ mGradientState.setCornerRadii(radii);
+ mPathIsDirty = true;
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the radii for each of the 4 corners. For each corner, the array
+ * contains 2 values, <code>[X_radius, Y_radius]</code>. The corners are
+ * ordered top-left, top-right, bottom-right, bottom-left.
+ * <p>
+ * If the radius was previously set with {@link #setCornerRadius(float)},
+ * or if the corners are not rounded, this method will return {@code null}.
+ *
+ * @return an array containing the radii for each of the 4 corners, or
+ * {@code null}
+ * @see #setCornerRadii(float[])
+ */
+ @Nullable
+ public float[] getCornerRadii() {
+ return mGradientState.mRadiusArray.clone();
+ }
+
+ /**
+ * Specifies the radius for the corners of the gradient. If this is > 0,
+ * then the drawable is drawn in a round-rectangle, rather than a
+ * rectangle. This property is honored only when the shape is of type
+ * {@link #RECTANGLE}.
+ * <p>
+ * <strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.
+ *
+ * @param radius The radius in pixels of the corners of the rectangle shape
+ *
+ * @see #mutate()
+ * @see #setCornerRadii(float[])
+ * @see #setShape(int)
+ */
+ public void setCornerRadius(float radius) {
+ mGradientState.setCornerRadius(radius);
+ mPathIsDirty = true;
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the radius for the corners of the gradient, that was previously set with
+ * {@link #setCornerRadius(float)}.
+ * <p>
+ * If the radius was previously cleared via passing {@code null}
+ * to {@link #setCornerRadii(float[])}, this method will return 0.
+ *
+ * @return the radius in pixels of the corners of the rectangle shape, or 0
+ * @see #setCornerRadius
+ */
+ public float getCornerRadius() {
+ return mGradientState.mRadius;
+ }
+
+ /**
+ * <p>Set the stroke width and color for the drawable. If width is zero,
+ * then no stroke is drawn.</p>
+ * <p><strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.</p>
+ *
+ * @param width The width in pixels of the stroke
+ * @param color The color of the stroke
+ *
+ * @see #mutate()
+ * @see #setStroke(int, int, float, float)
+ */
+ public void setStroke(int width, @ColorInt int color) {
+ setStroke(width, color, 0, 0);
+ }
+
+ /**
+ * <p>Set the stroke width and color state list for the drawable. If width
+ * is zero, then no stroke is drawn.</p>
+ * <p><strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.</p>
+ *
+ * @param width The width in pixels of the stroke
+ * @param colorStateList The color state list of the stroke
+ *
+ * @see #mutate()
+ * @see #setStroke(int, ColorStateList, float, float)
+ */
+ public void setStroke(int width, ColorStateList colorStateList) {
+ setStroke(width, colorStateList, 0, 0);
+ }
+
+ /**
+ * <p>Set the stroke width and color for the drawable. If width is zero,
+ * then no stroke is drawn. This method can also be used to dash the stroke.</p>
+ * <p><strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.</p>
+ *
+ * @param width The width in pixels of the stroke
+ * @param color The color of the stroke
+ * @param dashWidth The length in pixels of the dashes, set to 0 to disable dashes
+ * @param dashGap The gap in pixels between dashes
+ *
+ * @see #mutate()
+ * @see #setStroke(int, int)
+ */
+ public void setStroke(int width, @ColorInt int color, float dashWidth, float dashGap) {
+ mGradientState.setStroke(width, ColorStateList.valueOf(color), dashWidth, dashGap);
+ setStrokeInternal(width, color, dashWidth, dashGap);
+ }
+
+ /**
+ * <p>Set the stroke width and color state list for the drawable. If width
+ * is zero, then no stroke is drawn. This method can also be used to dash
+ * the stroke.</p>
+ * <p><strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.</p>
+ *
+ * @param width The width in pixels of the stroke
+ * @param colorStateList The color state list of the stroke
+ * @param dashWidth The length in pixels of the dashes, set to 0 to disable dashes
+ * @param dashGap The gap in pixels between dashes
+ *
+ * @see #mutate()
+ * @see #setStroke(int, ColorStateList)
+ */
+ public void setStroke(
+ int width, ColorStateList colorStateList, float dashWidth, float dashGap) {
+ mGradientState.setStroke(width, colorStateList, dashWidth, dashGap);
+ final int color;
+ if (colorStateList == null) {
+ color = Color.TRANSPARENT;
+ } else {
+ final int[] stateSet = getState();
+ color = colorStateList.getColorForState(stateSet, 0);
+ }
+ setStrokeInternal(width, color, dashWidth, dashGap);
+ }
+
+ private void setStrokeInternal(int width, int color, float dashWidth, float dashGap) {
+ if (mStrokePaint == null) {
+ mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mStrokePaint.setStyle(Paint.Style.STROKE);
+ }
+ mStrokePaint.setStrokeWidth(width);
+ mStrokePaint.setColor(color);
+
+ DashPathEffect e = null;
+ if (dashWidth > 0) {
+ e = new DashPathEffect(new float[] { dashWidth, dashGap }, 0);
+ }
+ mStrokePaint.setPathEffect(e);
+ invalidateSelf();
+ }
+
+
+ /**
+ * <p>Sets the size of the shape drawn by this drawable.</p>
+ * <p><strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.</p>
+ *
+ * @param width The width of the shape used by this drawable
+ * @param height The height of the shape used by this drawable
+ *
+ * @see #mutate()
+ * @see #setGradientType(int)
+ */
+ public void setSize(int width, int height) {
+ mGradientState.setSize(width, height);
+ mPathIsDirty = true;
+ invalidateSelf();
+ }
+
+ /**
+ * <p>Sets the type of shape used to draw the gradient.</p>
+ * <p><strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.</p>
+ *
+ * @param shape The desired shape for this drawable: {@link #LINE},
+ * {@link #OVAL}, {@link #RECTANGLE} or {@link #RING}
+ *
+ * @see #mutate()
+ */
+ public void setShape(@Shape int shape) {
+ mRingPath = null;
+ mPathIsDirty = true;
+ mGradientState.setShape(shape);
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the type of shape used by this drawable, one of {@link #LINE},
+ * {@link #OVAL}, {@link #RECTANGLE} or {@link #RING}.
+ *
+ * @return the type of shape used by this drawable
+ * @see #setShape(int)
+ */
+ @Shape
+ public int getShape() {
+ return mGradientState.mShape;
+ }
+
+ /**
+ * Sets the type of gradient used by this drawable.
+ * <p>
+ * <strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.
+ *
+ * @param gradient The type of the gradient: {@link #LINEAR_GRADIENT},
+ * {@link #RADIAL_GRADIENT} or {@link #SWEEP_GRADIENT}
+ *
+ * @see #mutate()
+ * @see #getGradientType()
+ */
+ public void setGradientType(@GradientType int gradient) {
+ mGradientState.setGradientType(gradient);
+ mGradientIsDirty = true;
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the type of gradient used by this drawable, one of
+ * {@link #LINEAR_GRADIENT}, {@link #RADIAL_GRADIENT}, or
+ * {@link #SWEEP_GRADIENT}.
+ *
+ * @return the type of gradient used by this drawable
+ * @see #setGradientType(int)
+ */
+ @GradientType
+ public int getGradientType() {
+ return mGradientState.mGradient;
+ }
+
+ /**
+ * Sets the position of the center of the gradient as a fraction of the
+ * width and height.
+ * <p>
+ * The default value is (0.5, 0.5).
+ * <p>
+ * <strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.
+ *
+ * @param x the X-position of the center of the gradient
+ * @param y the Y-position of the center of the gradient
+ *
+ * @see #mutate()
+ * @see #setGradientType(int)
+ * @see #getGradientCenterX()
+ * @see #getGradientCenterY()
+ */
+ public void setGradientCenter(float x, float y) {
+ mGradientState.setGradientCenter(x, y);
+ mGradientIsDirty = true;
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the X-position of the center of the gradient as a fraction of
+ * the width.
+ *
+ * @return the X-position of the center of the gradient
+ * @see #setGradientCenter(float, float)
+ */
+ public float getGradientCenterX() {
+ return mGradientState.mCenterX;
+ }
+
+ /**
+ * Returns the Y-position of the center of this gradient as a fraction of
+ * the height.
+ *
+ * @return the Y-position of the center of the gradient
+ * @see #setGradientCenter(float, float)
+ */
+ public float getGradientCenterY() {
+ return mGradientState.mCenterY;
+ }
+
+ /**
+ * Sets the radius of the gradient. The radius is honored only when the
+ * gradient type is set to {@link #RADIAL_GRADIENT}.
+ * <p>
+ * <strong>Note</strong>: changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.
+ *
+ * @param gradientRadius the radius of the gradient in pixels
+ *
+ * @see #mutate()
+ * @see #setGradientType(int)
+ * @see #getGradientRadius()
+ */
+ public void setGradientRadius(float gradientRadius) {
+ mGradientState.setGradientRadius(gradientRadius, TypedValue.COMPLEX_UNIT_PX);
+ mGradientIsDirty = true;
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the radius of the gradient in pixels. The radius is valid only
+ * when the gradient type is set to {@link #RADIAL_GRADIENT}.
+ *
+ * @return the radius of the gradient in pixels
+ * @see #setGradientRadius(float)
+ */
+ public float getGradientRadius() {
+ if (mGradientState.mGradient != RADIAL_GRADIENT) {
+ return 0;
+ }
+
+ ensureValidRect();
+ return mGradientRadius;
+ }
+
+ /**
+ * Sets whether this drawable's {@code level} property will be used to
+ * scale the gradient. If a gradient is not used, this property has no
+ * effect.
+ * <p>
+ * Scaling behavior varies based on gradient type:
+ * <ul>
+ * <li>{@link #LINEAR_GRADIENT} adjusts the ending position along the
+ * gradient's axis of orientation (see {@link #getOrientation()})
+ * <li>{@link #RADIAL_GRADIENT} adjusts the outer radius
+ * <li>{@link #SWEEP_GRADIENT} adjusts the ending angle
+ * <ul>
+ * <p>
+ * The default value for this property is {@code false}.
+ * <p>
+ * <strong>Note</strong>: This property corresponds to the
+ * {@code android:useLevel} attribute on the inner {@code <gradient>}
+ * tag, NOT the {@code android:useLevel} attribute on the outer
+ * {@code <shape>} tag. For example,
+ * <pre>{@code
+ * <shape ...>
+ * <gradient
+ * ...
+ * android:useLevel="true" />
+ * </shape>
+ * }</pre><p>
+ * <strong>Note</strong>: Changing this property will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing this property.
+ *
+ * @param useLevel {@code true} if the gradient should be scaled based on
+ * level, {@code false} otherwise
+ *
+ * @see #mutate()
+ * @see #setLevel(int)
+ * @see #getLevel()
+ * @see #getUseLevel()
+ * @attr ref android.R.styleable#GradientDrawableGradient_useLevel
+ */
+ public void setUseLevel(boolean useLevel) {
+ mGradientState.mUseLevel = useLevel;
+ mGradientIsDirty = true;
+ invalidateSelf();
+ }
+
+ /**
+ * Returns whether this drawable's {@code level} property will be used to
+ * scale the gradient.
+ *
+ * @return {@code true} if the gradient should be scaled based on level,
+ * {@code false} otherwise
+ * @see #setUseLevel(boolean)
+ * @attr ref android.R.styleable#GradientDrawableGradient_useLevel
+ */
+ public boolean getUseLevel() {
+ return mGradientState.mUseLevel;
+ }
+
+ private int modulateAlpha(int alpha) {
+ int scale = mAlpha + (mAlpha >> 7);
+ return alpha * scale >> 8;
+ }
+
+ /**
+ * Returns the orientation of the gradient defined in this drawable.
+ *
+ * @return the orientation of the gradient defined in this drawable
+ * @see #setOrientation(Orientation)
+ */
+ public Orientation getOrientation() {
+ return mGradientState.mOrientation;
+ }
+
+ /**
+ * Sets the orientation of the gradient defined in this drawable.
+ * <p>
+ * <strong>Note</strong>: changing orientation will affect all instances
+ * of a drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing the orientation.
+ *
+ * @param orientation the desired orientation (angle) of the gradient
+ *
+ * @see #mutate()
+ * @see #getOrientation()
+ */
+ public void setOrientation(Orientation orientation) {
+ mGradientState.mOrientation = orientation;
+ mGradientIsDirty = true;
+ invalidateSelf();
+ }
+
+ /**
+ * Sets the colors used to draw the gradient.
+ * <p>
+ * Each color is specified as an ARGB integer and the array must contain at
+ * least 2 colors.
+ * <p>
+ * <strong>Note</strong>: changing colors will affect all instances of a
+ * drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing the colors.
+ *
+ * @param colors an array containing 2 or more ARGB colors
+ * @see #mutate()
+ * @see #setColor(int)
+ */
+ public void setColors(@ColorInt int[] colors) {
+ mGradientState.setGradientColors(colors);
+ mGradientIsDirty = true;
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the colors used to draw the gradient, or {@code null} if the
+ * gradient is drawn using a single color or no colors.
+ *
+ * @return the colors used to draw the gradient, or {@code null}
+ * @see #setColors(int[] colors)
+ */
+ @Nullable
+ public int[] getColors() {
+ return mGradientState.mGradientColors == null ?
+ null : mGradientState.mGradientColors.clone();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (!ensureValidRect()) {
+ // nothing to draw
+ return;
+ }
+
+ // remember the alpha values, in case we temporarily overwrite them
+ // when we modulate them with mAlpha
+ final int prevFillAlpha = mFillPaint.getAlpha();
+ final int prevStrokeAlpha = mStrokePaint != null ? mStrokePaint.getAlpha() : 0;
+ // compute the modulate alpha values
+ final int currFillAlpha = modulateAlpha(prevFillAlpha);
+ final int currStrokeAlpha = modulateAlpha(prevStrokeAlpha);
+
+ final boolean haveStroke = currStrokeAlpha > 0 && mStrokePaint != null &&
+ mStrokePaint.getStrokeWidth() > 0;
+ final boolean haveFill = currFillAlpha > 0;
+ final GradientState st = mGradientState;
+ final ColorFilter colorFilter = mColorFilter != null ? mColorFilter : mTintFilter;
+
+ /* we need a layer iff we're drawing both a fill and stroke, and the
+ stroke is non-opaque, and our shapetype actually supports
+ fill+stroke. Otherwise we can just draw the stroke (if any) on top
+ of the fill (if any) without worrying about blending artifacts.
+ */
+ final boolean useLayer = haveStroke && haveFill && st.mShape != LINE &&
+ currStrokeAlpha < 255 && (mAlpha < 255 || colorFilter != null);
+
+ /* Drawing with a layer is slower than direct drawing, but it
+ allows us to apply paint effects like alpha and colorfilter to
+ the result of multiple separate draws. In our case, if the user
+ asks for a non-opaque alpha value (via setAlpha), and we're
+ stroking, then we need to apply the alpha AFTER we've drawn
+ both the fill and the stroke.
+ */
+ if (useLayer) {
+ if (mLayerPaint == null) {
+ mLayerPaint = new Paint();
+ }
+ mLayerPaint.setDither(st.mDither);
+ mLayerPaint.setAlpha(mAlpha);
+ mLayerPaint.setColorFilter(colorFilter);
+
+ float rad = mStrokePaint.getStrokeWidth();
+ canvas.saveLayer(mRect.left - rad, mRect.top - rad,
+ mRect.right + rad, mRect.bottom + rad,
+ mLayerPaint);
+
+ // don't perform the filter in our individual paints
+ // since the layer will do it for us
+ mFillPaint.setColorFilter(null);
+ mStrokePaint.setColorFilter(null);
+ } else {
+ /* if we're not using a layer, apply the dither/filter to our
+ individual paints
+ */
+ mFillPaint.setAlpha(currFillAlpha);
+ mFillPaint.setDither(st.mDither);
+ mFillPaint.setColorFilter(colorFilter);
+ if (colorFilter != null && st.mSolidColors == null) {
+ mFillPaint.setColor(mAlpha << 24);
+ }
+ if (haveStroke) {
+ mStrokePaint.setAlpha(currStrokeAlpha);
+ mStrokePaint.setDither(st.mDither);
+ mStrokePaint.setColorFilter(colorFilter);
+ }
+ }
+
+ switch (st.mShape) {
+ case RECTANGLE:
+ if (st.mRadiusArray != null) {
+ buildPathIfDirty();
+ canvas.drawPath(mPath, mFillPaint);
+ if (haveStroke) {
+ canvas.drawPath(mPath, mStrokePaint);
+ }
+ } else if (st.mRadius > 0.0f) {
+ // since the caller is only giving us 1 value, we will force
+ // it to be square if the rect is too small in one dimension
+ // to show it. If we did nothing, Skia would clamp the rad
+ // independently along each axis, giving us a thin ellipse
+ // if the rect were very wide but not very tall
+ float rad = Math.min(st.mRadius,
+ Math.min(mRect.width(), mRect.height()) * 0.5f);
+ canvas.drawRoundRect(mRect, rad, rad, mFillPaint);
+ if (haveStroke) {
+ canvas.drawRoundRect(mRect, rad, rad, mStrokePaint);
+ }
+ } else {
+ if (mFillPaint.getColor() != 0 || colorFilter != null ||
+ mFillPaint.getShader() != null) {
+ canvas.drawRect(mRect, mFillPaint);
+ }
+ if (haveStroke) {
+ canvas.drawRect(mRect, mStrokePaint);
+ }
+ }
+ break;
+ case OVAL:
+ canvas.drawOval(mRect, mFillPaint);
+ if (haveStroke) {
+ canvas.drawOval(mRect, mStrokePaint);
+ }
+ break;
+ case LINE: {
+ RectF r = mRect;
+ float y = r.centerY();
+ if (haveStroke) {
+ canvas.drawLine(r.left, y, r.right, y, mStrokePaint);
+ }
+ break;
+ }
+ case RING:
+ Path path = buildRing(st);
+ canvas.drawPath(path, mFillPaint);
+ if (haveStroke) {
+ canvas.drawPath(path, mStrokePaint);
+ }
+ break;
+ }
+
+ if (useLayer) {
+ canvas.restore();
+ } else {
+ mFillPaint.setAlpha(prevFillAlpha);
+ if (haveStroke) {
+ mStrokePaint.setAlpha(prevStrokeAlpha);
+ }
+ }
+ }
+
+ private void buildPathIfDirty() {
+ final GradientState st = mGradientState;
+ if (mPathIsDirty) {
+ ensureValidRect();
+ mPath.reset();
+ mPath.addRoundRect(mRect, st.mRadiusArray, Path.Direction.CW);
+ mPathIsDirty = false;
+ }
+ }
+
+ private Path buildRing(GradientState st) {
+ if (mRingPath != null && (!st.mUseLevelForShape || !mPathIsDirty)) return mRingPath;
+ mPathIsDirty = false;
+
+ float sweep = st.mUseLevelForShape ? (360.0f * getLevel() / 10000.0f) : 360f;
+
+ RectF bounds = new RectF(mRect);
+
+ float x = bounds.width() / 2.0f;
+ float y = bounds.height() / 2.0f;
+
+ float thickness = st.mThickness != -1 ?
+ st.mThickness : bounds.width() / st.mThicknessRatio;
+ // inner radius
+ float radius = st.mInnerRadius != -1 ?
+ st.mInnerRadius : bounds.width() / st.mInnerRadiusRatio;
+
+ RectF innerBounds = new RectF(bounds);
+ innerBounds.inset(x - radius, y - radius);
+
+ bounds = new RectF(innerBounds);
+ bounds.inset(-thickness, -thickness);
+
+ if (mRingPath == null) {
+ mRingPath = new Path();
+ } else {
+ mRingPath.reset();
+ }
+
+ final Path ringPath = mRingPath;
+ // arcTo treats the sweep angle mod 360, so check for that, since we
+ // think 360 means draw the entire oval
+ if (sweep < 360 && sweep > -360) {
+ ringPath.setFillType(Path.FillType.EVEN_ODD);
+ // inner top
+ ringPath.moveTo(x + radius, y);
+ // outer top
+ ringPath.lineTo(x + radius + thickness, y);
+ // outer arc
+ ringPath.arcTo(bounds, 0.0f, sweep, false);
+ // inner arc
+ ringPath.arcTo(innerBounds, sweep, -sweep, false);
+ ringPath.close();
+ } else {
+ // add the entire ovals
+ ringPath.addOval(bounds, Path.Direction.CW);
+ ringPath.addOval(innerBounds, Path.Direction.CCW);
+ }
+
+ return ringPath;
+ }
+
+ /**
+ * Changes this drawable to use a single color instead of a gradient.
+ * <p>
+ * <strong>Note</strong>: changing color will affect all instances of a
+ * drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing the color.
+ *
+ * @param argb The color used to fill the shape
+ *
+ * @see #mutate()
+ * @see #setColors(int[])
+ * @see #getColor
+ */
+ public void setColor(@ColorInt int argb) {
+ mGradientState.setSolidColors(ColorStateList.valueOf(argb));
+ mFillPaint.setColor(argb);
+ invalidateSelf();
+ }
+
+ /**
+ * Changes this drawable to use a single color state list instead of a
+ * gradient. Calling this method with a null argument will clear the color
+ * and is equivalent to calling {@link #setColor(int)} with the argument
+ * {@link Color#TRANSPARENT}.
+ * <p>
+ * <strong>Note</strong>: changing color will affect all instances of a
+ * drawable loaded from a resource. It is recommended to invoke
+ * {@link #mutate()} before changing the color.</p>
+ *
+ * @param colorStateList The color state list used to fill the shape
+ *
+ * @see #mutate()
+ * @see #getColor
+ */
+ public void setColor(@Nullable ColorStateList colorStateList) {
+ mGradientState.setSolidColors(colorStateList);
+ final int color;
+ if (colorStateList == null) {
+ color = Color.TRANSPARENT;
+ } else {
+ final int[] stateSet = getState();
+ color = colorStateList.getColorForState(stateSet, 0);
+ }
+ mFillPaint.setColor(color);
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the color state list used to fill the shape, or {@code null} if
+ * the shape is filled with a gradient or has no fill color.
+ *
+ * @return the color state list used to fill this gradient, or {@code null}
+ *
+ * @see #setColor(int)
+ * @see #setColor(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getColor() {
+ return mGradientState.mSolidColors;
+ }
+
+ @Override
+ protected boolean onStateChange(int[] stateSet) {
+ boolean invalidateSelf = false;
+
+ final GradientState s = mGradientState;
+ final ColorStateList solidColors = s.mSolidColors;
+ if (solidColors != null) {
+ final int newColor = solidColors.getColorForState(stateSet, 0);
+ final int oldColor = mFillPaint.getColor();
+ if (oldColor != newColor) {
+ mFillPaint.setColor(newColor);
+ invalidateSelf = true;
+ }
+ }
+
+ final Paint strokePaint = mStrokePaint;
+ if (strokePaint != null) {
+ final ColorStateList strokeColors = s.mStrokeColors;
+ if (strokeColors != null) {
+ final int newColor = strokeColors.getColorForState(stateSet, 0);
+ final int oldColor = strokePaint.getColor();
+ if (oldColor != newColor) {
+ strokePaint.setColor(newColor);
+ invalidateSelf = true;
+ }
+ }
+ }
+
+ if (s.mTint != null && s.mTintMode != null) {
+ mTintFilter = updateTintFilter(mTintFilter, s.mTint, s.mTintMode);
+ invalidateSelf = true;
+ }
+
+ if (invalidateSelf) {
+ invalidateSelf();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean isStateful() {
+ final GradientState s = mGradientState;
+ return super.isStateful()
+ || (s.mSolidColors != null && s.mSolidColors.isStateful())
+ || (s.mStrokeColors != null && s.mStrokeColors.isStateful())
+ || (s.mTint != null && s.mTint.isStateful());
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ final GradientState s = mGradientState;
+ return (s.mSolidColors != null && s.mSolidColors.hasFocusStateSpecified())
+ || (s.mStrokeColors != null && s.mStrokeColors.hasFocusStateSpecified())
+ || (s.mTint != null && s.mTint.hasFocusStateSpecified());
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations() | mGradientState.getChangingConfigurations();
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (alpha != mAlpha) {
+ mAlpha = alpha;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public void setDither(boolean dither) {
+ if (dither != mGradientState.mDither) {
+ mGradientState.mDither = dither;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ @Nullable
+ public ColorFilter getColorFilter() {
+ return mColorFilter;
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ if (colorFilter != mColorFilter) {
+ mColorFilter = colorFilter;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setTintList(@Nullable ColorStateList tint) {
+ mGradientState.mTint = tint;
+ mTintFilter = updateTintFilter(mTintFilter, tint, mGradientState.mTintMode);
+ invalidateSelf();
+ }
+
+ @Override
+ public void setTintMode(@Nullable PorterDuff.Mode tintMode) {
+ mGradientState.mTintMode = tintMode;
+ mTintFilter = updateTintFilter(mTintFilter, mGradientState.mTint, tintMode);
+ invalidateSelf();
+ }
+
+ @Override
+ public int getOpacity() {
+ return (mAlpha == 255 && mGradientState.mOpaqueOverBounds && isOpaqueForState()) ?
+ PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect r) {
+ super.onBoundsChange(r);
+ mRingPath = null;
+ mPathIsDirty = true;
+ mGradientIsDirty = true;
+ }
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ super.onLevelChange(level);
+ mGradientIsDirty = true;
+ mPathIsDirty = true;
+ invalidateSelf();
+ return true;
+ }
+
+ /**
+ * This checks mGradientIsDirty, and if it is true, recomputes both our drawing
+ * rectangle (mRect) and the gradient itself, since it depends on our
+ * rectangle too.
+ * @return true if the resulting rectangle is not empty, false otherwise
+ */
+ private boolean ensureValidRect() {
+ if (mGradientIsDirty) {
+ mGradientIsDirty = false;
+
+ Rect bounds = getBounds();
+ float inset = 0;
+
+ if (mStrokePaint != null) {
+ inset = mStrokePaint.getStrokeWidth() * 0.5f;
+ }
+
+ final GradientState st = mGradientState;
+
+ mRect.set(bounds.left + inset, bounds.top + inset,
+ bounds.right - inset, bounds.bottom - inset);
+
+ final int[] gradientColors = st.mGradientColors;
+ if (gradientColors != null) {
+ final RectF r = mRect;
+ final float x0, x1, y0, y1;
+
+ if (st.mGradient == LINEAR_GRADIENT) {
+ final float level = st.mUseLevel ? getLevel() / 10000.0f : 1.0f;
+ switch (st.mOrientation) {
+ case TOP_BOTTOM:
+ x0 = r.left; y0 = r.top;
+ x1 = x0; y1 = level * r.bottom;
+ break;
+ case TR_BL:
+ x0 = r.right; y0 = r.top;
+ x1 = level * r.left; y1 = level * r.bottom;
+ break;
+ case RIGHT_LEFT:
+ x0 = r.right; y0 = r.top;
+ x1 = level * r.left; y1 = y0;
+ break;
+ case BR_TL:
+ x0 = r.right; y0 = r.bottom;
+ x1 = level * r.left; y1 = level * r.top;
+ break;
+ case BOTTOM_TOP:
+ x0 = r.left; y0 = r.bottom;
+ x1 = x0; y1 = level * r.top;
+ break;
+ case BL_TR:
+ x0 = r.left; y0 = r.bottom;
+ x1 = level * r.right; y1 = level * r.top;
+ break;
+ case LEFT_RIGHT:
+ x0 = r.left; y0 = r.top;
+ x1 = level * r.right; y1 = y0;
+ break;
+ default:/* TL_BR */
+ x0 = r.left; y0 = r.top;
+ x1 = level * r.right; y1 = level * r.bottom;
+ break;
+ }
+
+ mFillPaint.setShader(new LinearGradient(x0, y0, x1, y1,
+ gradientColors, st.mPositions, Shader.TileMode.CLAMP));
+ } else if (st.mGradient == RADIAL_GRADIENT) {
+ x0 = r.left + (r.right - r.left) * st.mCenterX;
+ y0 = r.top + (r.bottom - r.top) * st.mCenterY;
+
+ float radius = st.mGradientRadius;
+ if (st.mGradientRadiusType == RADIUS_TYPE_FRACTION) {
+ // Fall back to parent width or height if intrinsic
+ // size is not specified.
+ final float width = st.mWidth >= 0 ? st.mWidth : r.width();
+ final float height = st.mHeight >= 0 ? st.mHeight : r.height();
+ radius *= Math.min(width, height);
+ } else if (st.mGradientRadiusType == RADIUS_TYPE_FRACTION_PARENT) {
+ radius *= Math.min(r.width(), r.height());
+ }
+
+ if (st.mUseLevel) {
+ radius *= getLevel() / 10000.0f;
+ }
+
+ mGradientRadius = radius;
+
+ if (radius <= 0) {
+ // We can't have a shader with non-positive radius, so
+ // let's have a very, very small radius.
+ radius = 0.001f;
+ }
+
+ mFillPaint.setShader(new RadialGradient(
+ x0, y0, radius, gradientColors, null, Shader.TileMode.CLAMP));
+ } else if (st.mGradient == SWEEP_GRADIENT) {
+ x0 = r.left + (r.right - r.left) * st.mCenterX;
+ y0 = r.top + (r.bottom - r.top) * st.mCenterY;
+
+ int[] tempColors = gradientColors;
+ float[] tempPositions = null;
+
+ if (st.mUseLevel) {
+ tempColors = st.mTempColors;
+ final int length = gradientColors.length;
+ if (tempColors == null || tempColors.length != length + 1) {
+ tempColors = st.mTempColors = new int[length + 1];
+ }
+ System.arraycopy(gradientColors, 0, tempColors, 0, length);
+ tempColors[length] = gradientColors[length - 1];
+
+ tempPositions = st.mTempPositions;
+ final float fraction = 1.0f / (length - 1);
+ if (tempPositions == null || tempPositions.length != length + 1) {
+ tempPositions = st.mTempPositions = new float[length + 1];
+ }
+
+ final float level = getLevel() / 10000.0f;
+ for (int i = 0; i < length; i++) {
+ tempPositions[i] = i * fraction * level;
+ }
+ tempPositions[length] = 1.0f;
+
+ }
+ mFillPaint.setShader(new SweepGradient(x0, y0, tempColors, tempPositions));
+ }
+
+ // If we don't have a solid color, the alpha channel must be
+ // maxed out so that alpha modulation works correctly.
+ if (st.mSolidColors == null) {
+ mFillPaint.setColor(Color.BLACK);
+ }
+ }
+ }
+ return !mRect.isEmpty();
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ super.inflate(r, parser, attrs, theme);
+
+ mGradientState.setDensity(Drawable.resolveDensity(r, 0));
+
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+
+ inflateChildElements(r, parser, attrs, theme);
+
+ updateLocalState(r);
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ final GradientState state = mGradientState;
+ if (state == null) {
+ return;
+ }
+
+ state.setDensity(Drawable.resolveDensity(t.getResources(), 0));
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(
+ state.mThemeAttrs, R.styleable.GradientDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+ }
+
+ if (state.mTint != null && state.mTint.canApplyTheme()) {
+ state.mTint = state.mTint.obtainForTheme(t);
+ }
+
+ if (state.mSolidColors != null && state.mSolidColors.canApplyTheme()) {
+ state.mSolidColors = state.mSolidColors.obtainForTheme(t);
+ }
+
+ if (state.mStrokeColors != null && state.mStrokeColors.canApplyTheme()) {
+ state.mStrokeColors = state.mStrokeColors.obtainForTheme(t);
+ }
+
+ applyThemeChildElements(t);
+
+ updateLocalState(t.getResources());
+ }
+
+ /**
+ * Updates the constant state from the values in the typed array.
+ */
+ private void updateStateFromTypedArray(TypedArray a) {
+ final GradientState state = mGradientState;
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ state.mShape = a.getInt(R.styleable.GradientDrawable_shape, state.mShape);
+ state.mDither = a.getBoolean(R.styleable.GradientDrawable_dither, state.mDither);
+
+ if (state.mShape == RING) {
+ state.mInnerRadius = a.getDimensionPixelSize(
+ R.styleable.GradientDrawable_innerRadius, state.mInnerRadius);
+
+ if (state.mInnerRadius == -1) {
+ state.mInnerRadiusRatio = a.getFloat(
+ R.styleable.GradientDrawable_innerRadiusRatio, state.mInnerRadiusRatio);
+ }
+
+ state.mThickness = a.getDimensionPixelSize(
+ R.styleable.GradientDrawable_thickness, state.mThickness);
+
+ if (state.mThickness == -1) {
+ state.mThicknessRatio = a.getFloat(
+ R.styleable.GradientDrawable_thicknessRatio, state.mThicknessRatio);
+ }
+
+ state.mUseLevelForShape = a.getBoolean(
+ R.styleable.GradientDrawable_useLevel, state.mUseLevelForShape);
+ }
+
+ final int tintMode = a.getInt(R.styleable.GradientDrawable_tintMode, -1);
+ if (tintMode != -1) {
+ state.mTintMode = Drawable.parseTintMode(tintMode, PorterDuff.Mode.SRC_IN);
+ }
+
+ final ColorStateList tint = a.getColorStateList(R.styleable.GradientDrawable_tint);
+ if (tint != null) {
+ state.mTint = tint;
+ }
+
+ final int insetLeft = a.getDimensionPixelSize(
+ R.styleable.GradientDrawable_opticalInsetLeft, state.mOpticalInsets.left);
+ final int insetTop = a.getDimensionPixelSize(
+ R.styleable.GradientDrawable_opticalInsetTop, state.mOpticalInsets.top);
+ final int insetRight = a.getDimensionPixelSize(
+ R.styleable.GradientDrawable_opticalInsetRight, state.mOpticalInsets.right);
+ final int insetBottom = a.getDimensionPixelSize(
+ R.styleable.GradientDrawable_opticalInsetBottom, state.mOpticalInsets.bottom);
+ state.mOpticalInsets = Insets.of(insetLeft, insetTop, insetRight, insetBottom);
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return (mGradientState != null && mGradientState.canApplyTheme()) || super.canApplyTheme();
+ }
+
+ private void applyThemeChildElements(Theme t) {
+ final GradientState st = mGradientState;
+
+ if (st.mAttrSize != null) {
+ final TypedArray a = t.resolveAttributes(
+ st.mAttrSize, R.styleable.GradientDrawableSize);
+ updateGradientDrawableSize(a);
+ a.recycle();
+ }
+
+ if (st.mAttrGradient != null) {
+ final TypedArray a = t.resolveAttributes(
+ st.mAttrGradient, R.styleable.GradientDrawableGradient);
+ try {
+ updateGradientDrawableGradient(t.getResources(), a);
+ } catch (XmlPullParserException e) {
+ rethrowAsRuntimeException(e);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ if (st.mAttrSolid != null) {
+ final TypedArray a = t.resolveAttributes(
+ st.mAttrSolid, R.styleable.GradientDrawableSolid);
+ updateGradientDrawableSolid(a);
+ a.recycle();
+ }
+
+ if (st.mAttrStroke != null) {
+ final TypedArray a = t.resolveAttributes(
+ st.mAttrStroke, R.styleable.GradientDrawableStroke);
+ updateGradientDrawableStroke(a);
+ a.recycle();
+ }
+
+ if (st.mAttrCorners != null) {
+ final TypedArray a = t.resolveAttributes(
+ st.mAttrCorners, R.styleable.DrawableCorners);
+ updateDrawableCorners(a);
+ a.recycle();
+ }
+
+ if (st.mAttrPadding != null) {
+ final TypedArray a = t.resolveAttributes(
+ st.mAttrPadding, R.styleable.GradientDrawablePadding);
+ updateGradientDrawablePadding(a);
+ a.recycle();
+ }
+ }
+
+ private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
+ Theme theme) throws XmlPullParserException, IOException {
+ TypedArray a;
+ int type;
+
+ final int innerDepth = parser.getDepth() + 1;
+ int depth;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth=parser.getDepth()) >= innerDepth
+ || type != XmlPullParser.END_TAG)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (depth > innerDepth) {
+ continue;
+ }
+
+ String name = parser.getName();
+
+ if (name.equals("size")) {
+ a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableSize);
+ updateGradientDrawableSize(a);
+ a.recycle();
+ } else if (name.equals("gradient")) {
+ a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableGradient);
+ updateGradientDrawableGradient(r, a);
+ a.recycle();
+ } else if (name.equals("solid")) {
+ a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableSolid);
+ updateGradientDrawableSolid(a);
+ a.recycle();
+ } else if (name.equals("stroke")) {
+ a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableStroke);
+ updateGradientDrawableStroke(a);
+ a.recycle();
+ } else if (name.equals("corners")) {
+ a = obtainAttributes(r, theme, attrs, R.styleable.DrawableCorners);
+ updateDrawableCorners(a);
+ a.recycle();
+ } else if (name.equals("padding")) {
+ a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawablePadding);
+ updateGradientDrawablePadding(a);
+ a.recycle();
+ } else {
+ Log.w("drawable", "Bad element under <shape>: " + name);
+ }
+ }
+ }
+
+ private void updateGradientDrawablePadding(TypedArray a) {
+ final GradientState st = mGradientState;
+
+ // Account for any configuration changes.
+ st.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ st.mAttrPadding = a.extractThemeAttrs();
+
+ if (st.mPadding == null) {
+ st.mPadding = new Rect();
+ }
+
+ final Rect pad = st.mPadding;
+ pad.set(a.getDimensionPixelOffset(R.styleable.GradientDrawablePadding_left, pad.left),
+ a.getDimensionPixelOffset(R.styleable.GradientDrawablePadding_top, pad.top),
+ a.getDimensionPixelOffset(R.styleable.GradientDrawablePadding_right, pad.right),
+ a.getDimensionPixelOffset(R.styleable.GradientDrawablePadding_bottom, pad.bottom));
+ mPadding = pad;
+ }
+
+ private void updateDrawableCorners(TypedArray a) {
+ final GradientState st = mGradientState;
+
+ // Account for any configuration changes.
+ st.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ st.mAttrCorners = a.extractThemeAttrs();
+
+ final int radius = a.getDimensionPixelSize(
+ R.styleable.DrawableCorners_radius, (int) st.mRadius);
+ setCornerRadius(radius);
+
+ // TODO: Update these to be themeable.
+ final int topLeftRadius = a.getDimensionPixelSize(
+ R.styleable.DrawableCorners_topLeftRadius, radius);
+ final int topRightRadius = a.getDimensionPixelSize(
+ R.styleable.DrawableCorners_topRightRadius, radius);
+ final int bottomLeftRadius = a.getDimensionPixelSize(
+ R.styleable.DrawableCorners_bottomLeftRadius, radius);
+ final int bottomRightRadius = a.getDimensionPixelSize(
+ R.styleable.DrawableCorners_bottomRightRadius, radius);
+ if (topLeftRadius != radius || topRightRadius != radius ||
+ bottomLeftRadius != radius || bottomRightRadius != radius) {
+ // The corner radii are specified in clockwise order (see Path.addRoundRect())
+ setCornerRadii(new float[] {
+ topLeftRadius, topLeftRadius,
+ topRightRadius, topRightRadius,
+ bottomRightRadius, bottomRightRadius,
+ bottomLeftRadius, bottomLeftRadius
+ });
+ }
+ }
+
+ private void updateGradientDrawableStroke(TypedArray a) {
+ final GradientState st = mGradientState;
+
+ // Account for any configuration changes.
+ st.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ st.mAttrStroke = a.extractThemeAttrs();
+
+ // We have an explicit stroke defined, so the default stroke width
+ // must be at least 0 or the current stroke width.
+ final int defaultStrokeWidth = Math.max(0, st.mStrokeWidth);
+ final int width = a.getDimensionPixelSize(
+ R.styleable.GradientDrawableStroke_width, defaultStrokeWidth);
+ final float dashWidth = a.getDimension(
+ R.styleable.GradientDrawableStroke_dashWidth, st.mStrokeDashWidth);
+
+ ColorStateList colorStateList = a.getColorStateList(
+ R.styleable.GradientDrawableStroke_color);
+ if (colorStateList == null) {
+ colorStateList = st.mStrokeColors;
+ }
+
+ if (dashWidth != 0.0f) {
+ final float dashGap = a.getDimension(
+ R.styleable.GradientDrawableStroke_dashGap, st.mStrokeDashGap);
+ setStroke(width, colorStateList, dashWidth, dashGap);
+ } else {
+ setStroke(width, colorStateList);
+ }
+ }
+
+ private void updateGradientDrawableSolid(TypedArray a) {
+ final GradientState st = mGradientState;
+
+ // Account for any configuration changes.
+ st.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ st.mAttrSolid = a.extractThemeAttrs();
+
+ final ColorStateList colorStateList = a.getColorStateList(
+ R.styleable.GradientDrawableSolid_color);
+ if (colorStateList != null) {
+ setColor(colorStateList);
+ }
+ }
+
+ private void updateGradientDrawableGradient(Resources r, TypedArray a)
+ throws XmlPullParserException {
+ final GradientState st = mGradientState;
+
+ // Account for any configuration changes.
+ st.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ st.mAttrGradient = a.extractThemeAttrs();
+
+ st.mCenterX = getFloatOrFraction(
+ a, R.styleable.GradientDrawableGradient_centerX, st.mCenterX);
+ st.mCenterY = getFloatOrFraction(
+ a, R.styleable.GradientDrawableGradient_centerY, st.mCenterY);
+ st.mUseLevel = a.getBoolean(
+ R.styleable.GradientDrawableGradient_useLevel, st.mUseLevel);
+ st.mGradient = a.getInt(
+ R.styleable.GradientDrawableGradient_type, st.mGradient);
+
+ // TODO: Update these to be themeable.
+ final int startColor = a.getColor(
+ R.styleable.GradientDrawableGradient_startColor, 0);
+ final boolean hasCenterColor = a.hasValue(
+ R.styleable.GradientDrawableGradient_centerColor);
+ final int centerColor = a.getColor(
+ R.styleable.GradientDrawableGradient_centerColor, 0);
+ final int endColor = a.getColor(
+ R.styleable.GradientDrawableGradient_endColor, 0);
+
+ if (hasCenterColor) {
+ st.mGradientColors = new int[3];
+ st.mGradientColors[0] = startColor;
+ st.mGradientColors[1] = centerColor;
+ st.mGradientColors[2] = endColor;
+
+ st.mPositions = new float[3];
+ st.mPositions[0] = 0.0f;
+ // Since 0.5f is default value, try to take the one that isn't 0.5f
+ st.mPositions[1] = st.mCenterX != 0.5f ? st.mCenterX : st.mCenterY;
+ st.mPositions[2] = 1f;
+ } else {
+ st.mGradientColors = new int[2];
+ st.mGradientColors[0] = startColor;
+ st.mGradientColors[1] = endColor;
+ }
+
+ if (st.mGradient == LINEAR_GRADIENT) {
+ int angle = (int) a.getFloat(R.styleable.GradientDrawableGradient_angle, st.mAngle);
+ angle %= 360;
+
+ if (angle % 45 != 0) {
+ throw new XmlPullParserException(a.getPositionDescription()
+ + "<gradient> tag requires 'angle' attribute to "
+ + "be a multiple of 45");
+ }
+
+ st.mAngle = angle;
+
+ switch (angle) {
+ case 0:
+ st.mOrientation = Orientation.LEFT_RIGHT;
+ break;
+ case 45:
+ st.mOrientation = Orientation.BL_TR;
+ break;
+ case 90:
+ st.mOrientation = Orientation.BOTTOM_TOP;
+ break;
+ case 135:
+ st.mOrientation = Orientation.BR_TL;
+ break;
+ case 180:
+ st.mOrientation = Orientation.RIGHT_LEFT;
+ break;
+ case 225:
+ st.mOrientation = Orientation.TR_BL;
+ break;
+ case 270:
+ st.mOrientation = Orientation.TOP_BOTTOM;
+ break;
+ case 315:
+ st.mOrientation = Orientation.TL_BR;
+ break;
+ }
+ } else {
+ final TypedValue tv = a.peekValue(R.styleable.GradientDrawableGradient_gradientRadius);
+ if (tv != null) {
+ final float radius;
+ final @RadiusType int radiusType;
+ if (tv.type == TypedValue.TYPE_FRACTION) {
+ radius = tv.getFraction(1.0f, 1.0f);
+
+ final int unit = (tv.data >> TypedValue.COMPLEX_UNIT_SHIFT)
+ & TypedValue.COMPLEX_UNIT_MASK;
+ if (unit == TypedValue.COMPLEX_UNIT_FRACTION_PARENT) {
+ radiusType = RADIUS_TYPE_FRACTION_PARENT;
+ } else {
+ radiusType = RADIUS_TYPE_FRACTION;
+ }
+ } else if (tv.type == TypedValue.TYPE_DIMENSION) {
+ radius = tv.getDimension(r.getDisplayMetrics());
+ radiusType = RADIUS_TYPE_PIXELS;
+ } else {
+ radius = tv.getFloat();
+ radiusType = RADIUS_TYPE_PIXELS;
+ }
+
+ st.mGradientRadius = radius;
+ st.mGradientRadiusType = radiusType;
+ } else if (st.mGradient == RADIAL_GRADIENT) {
+ throw new XmlPullParserException(
+ a.getPositionDescription()
+ + "<gradient> tag requires 'gradientRadius' "
+ + "attribute with radial type");
+ }
+ }
+ }
+
+ private void updateGradientDrawableSize(TypedArray a) {
+ final GradientState st = mGradientState;
+
+ // Account for any configuration changes.
+ st.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ st.mAttrSize = a.extractThemeAttrs();
+
+ st.mWidth = a.getDimensionPixelSize(R.styleable.GradientDrawableSize_width, st.mWidth);
+ st.mHeight = a.getDimensionPixelSize(R.styleable.GradientDrawableSize_height, st.mHeight);
+ }
+
+ private static float getFloatOrFraction(TypedArray a, int index, float defaultValue) {
+ TypedValue tv = a.peekValue(index);
+ float v = defaultValue;
+ if (tv != null) {
+ boolean vIsFraction = tv.type == TypedValue.TYPE_FRACTION;
+ v = vIsFraction ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
+ }
+ return v;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mGradientState.mWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mGradientState.mHeight;
+ }
+
+ /** @hide */
+ @Override
+ public Insets getOpticalInsets() {
+ return mGradientState.mOpticalInsets;
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ mGradientState.mChangingConfigurations = getChangingConfigurations();
+ return mGradientState;
+ }
+
+ private boolean isOpaqueForState() {
+ if (mGradientState.mStrokeWidth >= 0 && mStrokePaint != null
+ && !isOpaque(mStrokePaint.getColor())) {
+ return false;
+ }
+
+ // Don't check opacity if we're using a gradient, as we've already
+ // checked the gradient opacity in mOpaqueOverShape.
+ if (mGradientState.mGradientColors == null && !isOpaque(mFillPaint.getColor())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void getOutline(Outline outline) {
+ final GradientState st = mGradientState;
+ final Rect bounds = getBounds();
+ // only report non-zero alpha if shape being drawn has consistent opacity over shape. Must
+ // either not have a stroke, or have same stroke/fill opacity
+ boolean useFillOpacity = st.mOpaqueOverShape && (mGradientState.mStrokeWidth <= 0
+ || mStrokePaint == null
+ || mStrokePaint.getAlpha() == mFillPaint.getAlpha());
+ outline.setAlpha(useFillOpacity
+ ? modulateAlpha(mFillPaint.getAlpha()) / 255.0f
+ : 0.0f);
+
+ switch (st.mShape) {
+ case RECTANGLE:
+ if (st.mRadiusArray != null) {
+ buildPathIfDirty();
+ outline.setConvexPath(mPath);
+ return;
+ }
+
+ float rad = 0;
+ if (st.mRadius > 0.0f) {
+ // clamp the radius based on width & height, matching behavior in draw()
+ rad = Math.min(st.mRadius,
+ Math.min(bounds.width(), bounds.height()) * 0.5f);
+ }
+ outline.setRoundRect(bounds, rad);
+ return;
+ case OVAL:
+ outline.setOval(bounds);
+ return;
+ case LINE:
+ // Hairlines (0-width stroke) must have a non-empty outline for
+ // shadows to draw correctly, so we'll use a very small width.
+ final float halfStrokeWidth = mStrokePaint == null ?
+ 0.0001f : mStrokePaint.getStrokeWidth() * 0.5f;
+ final float centerY = bounds.centerY();
+ final int top = (int) Math.floor(centerY - halfStrokeWidth);
+ final int bottom = (int) Math.ceil(centerY + halfStrokeWidth);
+
+ outline.setRect(bounds.left, top, bounds.right, bottom);
+ return;
+ default:
+ // TODO: support more complex shapes
+ }
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mGradientState = new GradientState(mGradientState, null);
+ updateLocalState(null);
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mMutated = false;
+ }
+
+ final static class GradientState extends ConstantState {
+ public @Config int mChangingConfigurations;
+ public @Shape int mShape = RECTANGLE;
+ public @GradientType int mGradient = LINEAR_GRADIENT;
+ public int mAngle = 0;
+ public Orientation mOrientation;
+ public ColorStateList mSolidColors;
+ public ColorStateList mStrokeColors;
+ public @ColorInt int[] mGradientColors;
+ public @ColorInt int[] mTempColors; // no need to copy
+ public float[] mTempPositions; // no need to copy
+ public float[] mPositions;
+ public int mStrokeWidth = -1; // if >= 0 use stroking.
+ public float mStrokeDashWidth = 0.0f;
+ public float mStrokeDashGap = 0.0f;
+ public float mRadius = 0.0f; // use this if mRadiusArray is null
+ public float[] mRadiusArray = null;
+ public Rect mPadding = null;
+ public int mWidth = -1;
+ public int mHeight = -1;
+ public float mInnerRadiusRatio = DEFAULT_INNER_RADIUS_RATIO;
+ public float mThicknessRatio = DEFAULT_THICKNESS_RATIO;
+ public int mInnerRadius = -1;
+ public int mThickness = -1;
+ public boolean mDither = false;
+ public Insets mOpticalInsets = Insets.NONE;
+
+ float mCenterX = 0.5f;
+ float mCenterY = 0.5f;
+ float mGradientRadius = 0.5f;
+ @RadiusType int mGradientRadiusType = RADIUS_TYPE_PIXELS;
+ boolean mUseLevel = false;
+ boolean mUseLevelForShape = true;
+
+ boolean mOpaqueOverBounds;
+ boolean mOpaqueOverShape;
+
+ ColorStateList mTint = null;
+ PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;
+
+ int mDensity = DisplayMetrics.DENSITY_DEFAULT;
+
+ int[] mThemeAttrs;
+ int[] mAttrSize;
+ int[] mAttrGradient;
+ int[] mAttrSolid;
+ int[] mAttrStroke;
+ int[] mAttrCorners;
+ int[] mAttrPadding;
+
+ public GradientState(Orientation orientation, int[] gradientColors) {
+ mOrientation = orientation;
+ setGradientColors(gradientColors);
+ }
+
+ public GradientState(@NonNull GradientState orig, @Nullable Resources res) {
+ mChangingConfigurations = orig.mChangingConfigurations;
+ mShape = orig.mShape;
+ mGradient = orig.mGradient;
+ mAngle = orig.mAngle;
+ mOrientation = orig.mOrientation;
+ mSolidColors = orig.mSolidColors;
+ if (orig.mGradientColors != null) {
+ mGradientColors = orig.mGradientColors.clone();
+ }
+ if (orig.mPositions != null) {
+ mPositions = orig.mPositions.clone();
+ }
+ mStrokeColors = orig.mStrokeColors;
+ mStrokeWidth = orig.mStrokeWidth;
+ mStrokeDashWidth = orig.mStrokeDashWidth;
+ mStrokeDashGap = orig.mStrokeDashGap;
+ mRadius = orig.mRadius;
+ if (orig.mRadiusArray != null) {
+ mRadiusArray = orig.mRadiusArray.clone();
+ }
+ if (orig.mPadding != null) {
+ mPadding = new Rect(orig.mPadding);
+ }
+ mWidth = orig.mWidth;
+ mHeight = orig.mHeight;
+ mInnerRadiusRatio = orig.mInnerRadiusRatio;
+ mThicknessRatio = orig.mThicknessRatio;
+ mInnerRadius = orig.mInnerRadius;
+ mThickness = orig.mThickness;
+ mDither = orig.mDither;
+ mOpticalInsets = orig.mOpticalInsets;
+ mCenterX = orig.mCenterX;
+ mCenterY = orig.mCenterY;
+ mGradientRadius = orig.mGradientRadius;
+ mGradientRadiusType = orig.mGradientRadiusType;
+ mUseLevel = orig.mUseLevel;
+ mUseLevelForShape = orig.mUseLevelForShape;
+ mOpaqueOverBounds = orig.mOpaqueOverBounds;
+ mOpaqueOverShape = orig.mOpaqueOverShape;
+ mTint = orig.mTint;
+ mTintMode = orig.mTintMode;
+ mThemeAttrs = orig.mThemeAttrs;
+ mAttrSize = orig.mAttrSize;
+ mAttrGradient = orig.mAttrGradient;
+ mAttrSolid = orig.mAttrSolid;
+ mAttrStroke = orig.mAttrStroke;
+ mAttrCorners = orig.mAttrCorners;
+ mAttrPadding = orig.mAttrPadding;
+
+ mDensity = Drawable.resolveDensity(res, orig.mDensity);
+ if (orig.mDensity != mDensity) {
+ applyDensityScaling(orig.mDensity, mDensity);
+ }
+ }
+
+ /**
+ * Sets the constant state density.
+ * <p>
+ * If the density has been previously set, dispatches the change to
+ * subclasses so that density-dependent properties may be scaled as
+ * necessary.
+ *
+ * @param targetDensity the new constant state density
+ */
+ public final void setDensity(int targetDensity) {
+ if (mDensity != targetDensity) {
+ final int sourceDensity = mDensity;
+ mDensity = targetDensity;
+
+ applyDensityScaling(sourceDensity, targetDensity);
+ }
+ }
+
+ private void applyDensityScaling(int sourceDensity, int targetDensity) {
+ if (mInnerRadius > 0) {
+ mInnerRadius = Drawable.scaleFromDensity(
+ mInnerRadius, sourceDensity, targetDensity, true);
+ }
+ if (mThickness > 0) {
+ mThickness = Drawable.scaleFromDensity(
+ mThickness, sourceDensity, targetDensity, true);
+ }
+ if (mOpticalInsets != Insets.NONE) {
+ final int left = Drawable.scaleFromDensity(
+ mOpticalInsets.left, sourceDensity, targetDensity, true);
+ final int top = Drawable.scaleFromDensity(
+ mOpticalInsets.top, sourceDensity, targetDensity, true);
+ final int right = Drawable.scaleFromDensity(
+ mOpticalInsets.right, sourceDensity, targetDensity, true);
+ final int bottom = Drawable.scaleFromDensity(
+ mOpticalInsets.bottom, sourceDensity, targetDensity, true);
+ mOpticalInsets = Insets.of(left, top, right, bottom);
+ }
+ if (mPadding != null) {
+ mPadding.left = Drawable.scaleFromDensity(
+ mPadding.left, sourceDensity, targetDensity, false);
+ mPadding.top = Drawable.scaleFromDensity(
+ mPadding.top, sourceDensity, targetDensity, false);
+ mPadding.right = Drawable.scaleFromDensity(
+ mPadding.right, sourceDensity, targetDensity, false);
+ mPadding.bottom = Drawable.scaleFromDensity(
+ mPadding.bottom, sourceDensity, targetDensity, false);
+ }
+ if (mRadius > 0) {
+ mRadius = Drawable.scaleFromDensity(mRadius, sourceDensity, targetDensity);
+ }
+ if (mRadiusArray != null) {
+ mRadiusArray[0] = Drawable.scaleFromDensity(
+ (int) mRadiusArray[0], sourceDensity, targetDensity, true);
+ mRadiusArray[1] = Drawable.scaleFromDensity(
+ (int) mRadiusArray[1], sourceDensity, targetDensity, true);
+ mRadiusArray[2] = Drawable.scaleFromDensity(
+ (int) mRadiusArray[2], sourceDensity, targetDensity, true);
+ mRadiusArray[3] = Drawable.scaleFromDensity(
+ (int) mRadiusArray[3], sourceDensity, targetDensity, true);
+ }
+ if (mStrokeWidth > 0) {
+ mStrokeWidth = Drawable.scaleFromDensity(
+ mStrokeWidth, sourceDensity, targetDensity, true);
+ }
+ if (mStrokeDashWidth > 0) {
+ mStrokeDashWidth = Drawable.scaleFromDensity(
+ mStrokeDashGap, sourceDensity, targetDensity);
+ }
+ if (mStrokeDashGap > 0) {
+ mStrokeDashGap = Drawable.scaleFromDensity(
+ mStrokeDashGap, sourceDensity, targetDensity);
+ }
+ if (mGradientRadiusType == RADIUS_TYPE_PIXELS) {
+ mGradientRadius = Drawable.scaleFromDensity(
+ mGradientRadius, sourceDensity, targetDensity);
+ }
+ if (mWidth > 0) {
+ mWidth = Drawable.scaleFromDensity(mWidth, sourceDensity, targetDensity, true);
+ }
+ if (mHeight > 0) {
+ mHeight = Drawable.scaleFromDensity(mHeight, sourceDensity, targetDensity, true);
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mThemeAttrs != null
+ || mAttrSize != null || mAttrGradient != null
+ || mAttrSolid != null || mAttrStroke != null
+ || mAttrCorners != null || mAttrPadding != null
+ || (mTint != null && mTint.canApplyTheme())
+ || (mStrokeColors != null && mStrokeColors.canApplyTheme())
+ || (mSolidColors != null && mSolidColors.canApplyTheme())
+ || super.canApplyTheme();
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new GradientDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(@Nullable Resources res) {
+ // If this drawable is being created for a different density,
+ // just create a new constant state and call it a day.
+ final GradientState state;
+ final int density = Drawable.resolveDensity(res, mDensity);
+ if (density != mDensity) {
+ state = new GradientState(this, res);
+ } else {
+ state = this;
+ }
+
+ return new GradientDrawable(state, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations
+ | (mStrokeColors != null ? mStrokeColors.getChangingConfigurations() : 0)
+ | (mSolidColors != null ? mSolidColors.getChangingConfigurations() : 0)
+ | (mTint != null ? mTint.getChangingConfigurations() : 0);
+ }
+
+ public void setShape(@Shape int shape) {
+ mShape = shape;
+ computeOpacity();
+ }
+
+ public void setGradientType(@GradientType int gradient) {
+ mGradient = gradient;
+ }
+
+ public void setGradientCenter(float x, float y) {
+ mCenterX = x;
+ mCenterY = y;
+ }
+
+ public void setGradientColors(@Nullable int[] colors) {
+ mGradientColors = colors;
+ mSolidColors = null;
+ computeOpacity();
+ }
+
+ public void setSolidColors(@Nullable ColorStateList colors) {
+ mGradientColors = null;
+ mSolidColors = colors;
+ computeOpacity();
+ }
+
+ private void computeOpacity() {
+ mOpaqueOverBounds = false;
+ mOpaqueOverShape = false;
+
+ if (mGradientColors != null) {
+ for (int i = 0; i < mGradientColors.length; i++) {
+ if (!isOpaque(mGradientColors[i])) {
+ return;
+ }
+ }
+ }
+
+ // An unfilled shape is not opaque over bounds or shape
+ if (mGradientColors == null && mSolidColors == null) {
+ return;
+ }
+
+ // Colors are opaque, so opaqueOverShape=true,
+ mOpaqueOverShape = true;
+ // and opaqueOverBounds=true if shape fills bounds
+ mOpaqueOverBounds = mShape == RECTANGLE
+ && mRadius <= 0
+ && mRadiusArray == null;
+ }
+
+ public void setStroke(int width, @Nullable ColorStateList colors, float dashWidth,
+ float dashGap) {
+ mStrokeWidth = width;
+ mStrokeColors = colors;
+ mStrokeDashWidth = dashWidth;
+ mStrokeDashGap = dashGap;
+ computeOpacity();
+ }
+
+ public void setCornerRadius(float radius) {
+ if (radius < 0) {
+ radius = 0;
+ }
+ mRadius = radius;
+ mRadiusArray = null;
+ }
+
+ public void setCornerRadii(float[] radii) {
+ mRadiusArray = radii;
+ if (radii == null) {
+ mRadius = 0;
+ }
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ public void setGradientRadius(float gradientRadius, @RadiusType int type) {
+ mGradientRadius = gradientRadius;
+ mGradientRadiusType = type;
+ }
+ }
+
+ static boolean isOpaque(int color) {
+ return ((color >> 24) & 0xff) == 0xff;
+ }
+
+ /**
+ * Creates a new themed GradientDrawable based on the specified constant state.
+ * <p>
+ * The resulting drawable is guaranteed to have a new constant state.
+ *
+ * @param state Constant state from which the drawable inherits
+ */
+ private GradientDrawable(@NonNull GradientState state, @Nullable Resources res) {
+ mGradientState = state;
+
+ updateLocalState(res);
+ }
+
+ private void updateLocalState(Resources res) {
+ final GradientState state = mGradientState;
+
+ if (state.mSolidColors != null) {
+ final int[] currentState = getState();
+ final int stateColor = state.mSolidColors.getColorForState(currentState, 0);
+ mFillPaint.setColor(stateColor);
+ } else if (state.mGradientColors == null) {
+ // If we don't have a solid color and we don't have a gradient,
+ // the app is stroking the shape, set the color to the default
+ // value of state.mSolidColor
+ mFillPaint.setColor(0);
+ } else {
+ // Otherwise, make sure the fill alpha is maxed out.
+ mFillPaint.setColor(Color.BLACK);
+ }
+
+ mPadding = state.mPadding;
+
+ if (state.mStrokeWidth >= 0) {
+ mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mStrokePaint.setStyle(Paint.Style.STROKE);
+ mStrokePaint.setStrokeWidth(state.mStrokeWidth);
+
+ if (state.mStrokeColors != null) {
+ final int[] currentState = getState();
+ final int strokeStateColor = state.mStrokeColors.getColorForState(
+ currentState, 0);
+ mStrokePaint.setColor(strokeStateColor);
+ }
+
+ if (state.mStrokeDashWidth != 0.0f) {
+ final DashPathEffect e = new DashPathEffect(
+ new float[] { state.mStrokeDashWidth, state.mStrokeDashGap }, 0);
+ mStrokePaint.setPathEffect(e);
+ }
+ }
+
+ mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
+ mGradientIsDirty = true;
+
+ state.computeOpacity();
+ }
+}
diff --git a/android/graphics/drawable/GradientDrawable_Delegate.java b/android/graphics/drawable/GradientDrawable_Delegate.java
new file mode 100644
index 0000000..a3ad2aa
--- /dev/null
+++ b/android/graphics/drawable/GradientDrawable_Delegate.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015 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 android.graphics.drawable;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.graphics.Path;
+import android.graphics.drawable.GradientDrawable.GradientState;
+
+import java.lang.reflect.Field;
+
+/**
+ * Delegate implementing the native methods of {@link GradientDrawable}
+ *
+ * Through the layoutlib_create tool, the original native methods of GradientDrawable have been
+ * replaced by calls to methods of the same name in this delegate class.
+ */
+public class GradientDrawable_Delegate {
+
+ /**
+ * The ring can be built either by drawing full circles, or by drawing arcs in case the
+ * circle isn't complete. LayoutLib cannot handle drawing full circles (requires path
+ * subtraction). So, if we need to draw full circles, we switch to drawing 99% circle.
+ */
+ @LayoutlibDelegate
+ /*package*/ static Path buildRing(GradientDrawable thisDrawable, GradientState st) {
+ boolean useLevel = st.mUseLevelForShape;
+ int level = thisDrawable.getLevel();
+ // 10000 is the max level. See android.graphics.drawable.Drawable#getLevel()
+ float sweep = useLevel ? (360.0f * level / 10000.0f) : 360f;
+ Field mLevel = null;
+ if (sweep >= 360 || sweep <= -360) {
+ st.mUseLevelForShape = true;
+ // Use reflection to set the value of the field to prevent setting the drawable to
+ // dirty again.
+ try {
+ mLevel = Drawable.class.getDeclaredField("mLevel");
+ mLevel.setAccessible(true);
+ mLevel.setInt(thisDrawable, 9999); // set to one less than max.
+ } catch (NoSuchFieldException e) {
+ // The field has been removed in a recent framework change. Fall back to old
+ // buggy behaviour.
+ } catch (IllegalAccessException e) {
+ // We've already set the field to be accessible.
+ assert false;
+ }
+ }
+ Path path = thisDrawable.buildRing_Original(st);
+ st.mUseLevelForShape = useLevel;
+ if (mLevel != null) {
+ try {
+ mLevel.setInt(thisDrawable, level);
+ } catch (IllegalAccessException e) {
+ assert false;
+ }
+ }
+ return path;
+ }
+}
diff --git a/android/graphics/drawable/Icon.java b/android/graphics/drawable/Icon.java
new file mode 100644
index 0000000..c329918
--- /dev/null
+++ b/android/graphics/drawable/Icon.java
@@ -0,0 +1,887 @@
+/*
+ * Copyright (C) 2015 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 android.graphics.drawable;
+
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.content.res.ColorStateList;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.PorterDuff;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * An umbrella container for several serializable graphics representations, including Bitmaps,
+ * compressed bitmap images (e.g. JPG or PNG), and drawable resources (including vectors).
+ *
+ * <a href="https://developer.android.com/training/displaying-bitmaps/index.html">Much ink</a>
+ * has been spilled on the best way to load images, and many clients may have different needs when
+ * it comes to threading and fetching. This class is therefore focused on encapsulation rather than
+ * behavior.
+ */
+
+public final class Icon implements Parcelable {
+ private static final String TAG = "Icon";
+
+ /** @hide */
+ public static final int TYPE_BITMAP = 1;
+ /** @hide */
+ public static final int TYPE_RESOURCE = 2;
+ /** @hide */
+ public static final int TYPE_DATA = 3;
+ /** @hide */
+ public static final int TYPE_URI = 4;
+ /** @hide */
+ public static final int TYPE_ADAPTIVE_BITMAP = 5;
+
+ private static final int VERSION_STREAM_SERIALIZER = 1;
+
+ private final int mType;
+
+ private ColorStateList mTintList;
+ static final PorterDuff.Mode DEFAULT_TINT_MODE = Drawable.DEFAULT_TINT_MODE; // SRC_IN
+ private PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;
+
+ // To avoid adding unnecessary overhead, we have a few basic objects that get repurposed
+ // based on the value of mType.
+
+ // TYPE_BITMAP: Bitmap
+ // TYPE_RESOURCE: Resources
+ // TYPE_DATA: DataBytes
+ private Object mObj1;
+
+ // TYPE_RESOURCE: package name
+ // TYPE_URI: uri string
+ private String mString1;
+
+ // TYPE_RESOURCE: resId
+ // TYPE_DATA: data length
+ private int mInt1;
+
+ // TYPE_DATA: data offset
+ private int mInt2;
+
+ /**
+ * @return The type of image data held in this Icon. One of
+ * {@link #TYPE_BITMAP},
+ * {@link #TYPE_RESOURCE},
+ * {@link #TYPE_DATA}, or
+ * {@link #TYPE_URI}.
+ * {@link #TYPE_ADAPTIVE_BITMAP}
+ * @hide
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * @return The {@link android.graphics.Bitmap} held by this {@link #TYPE_BITMAP} Icon.
+ * @hide
+ */
+ public Bitmap getBitmap() {
+ if (mType != TYPE_BITMAP && mType != TYPE_ADAPTIVE_BITMAP) {
+ throw new IllegalStateException("called getBitmap() on " + this);
+ }
+ return (Bitmap) mObj1;
+ }
+
+ private void setBitmap(Bitmap b) {
+ mObj1 = b;
+ }
+
+ /**
+ * @return The length of the compressed bitmap byte array held by this {@link #TYPE_DATA} Icon.
+ * @hide
+ */
+ public int getDataLength() {
+ if (mType != TYPE_DATA) {
+ throw new IllegalStateException("called getDataLength() on " + this);
+ }
+ synchronized (this) {
+ return mInt1;
+ }
+ }
+
+ /**
+ * @return The offset into the byte array held by this {@link #TYPE_DATA} Icon at which
+ * valid compressed bitmap data is found.
+ * @hide
+ */
+ public int getDataOffset() {
+ if (mType != TYPE_DATA) {
+ throw new IllegalStateException("called getDataOffset() on " + this);
+ }
+ synchronized (this) {
+ return mInt2;
+ }
+ }
+
+ /**
+ * @return The byte array held by this {@link #TYPE_DATA} Icon ctonaining compressed
+ * bitmap data.
+ * @hide
+ */
+ public byte[] getDataBytes() {
+ if (mType != TYPE_DATA) {
+ throw new IllegalStateException("called getDataBytes() on " + this);
+ }
+ synchronized (this) {
+ return (byte[]) mObj1;
+ }
+ }
+
+ /**
+ * @return The {@link android.content.res.Resources} for this {@link #TYPE_RESOURCE} Icon.
+ * @hide
+ */
+ public Resources getResources() {
+ if (mType != TYPE_RESOURCE) {
+ throw new IllegalStateException("called getResources() on " + this);
+ }
+ return (Resources) mObj1;
+ }
+
+ /**
+ * @return The package containing resources for this {@link #TYPE_RESOURCE} Icon.
+ * @hide
+ */
+ public String getResPackage() {
+ if (mType != TYPE_RESOURCE) {
+ throw new IllegalStateException("called getResPackage() on " + this);
+ }
+ return mString1;
+ }
+
+ /**
+ * @return The resource ID for this {@link #TYPE_RESOURCE} Icon.
+ * @hide
+ */
+ public int getResId() {
+ if (mType != TYPE_RESOURCE) {
+ throw new IllegalStateException("called getResId() on " + this);
+ }
+ return mInt1;
+ }
+
+ /**
+ * @return The URI (as a String) for this {@link #TYPE_URI} Icon.
+ * @hide
+ */
+ public String getUriString() {
+ if (mType != TYPE_URI) {
+ throw new IllegalStateException("called getUriString() on " + this);
+ }
+ return mString1;
+ }
+
+ /**
+ * @return The {@link android.net.Uri} for this {@link #TYPE_URI} Icon.
+ * @hide
+ */
+ public Uri getUri() {
+ return Uri.parse(getUriString());
+ }
+
+ private static final String typeToString(int x) {
+ switch (x) {
+ case TYPE_BITMAP: return "BITMAP";
+ case TYPE_ADAPTIVE_BITMAP: return "BITMAP_MASKABLE";
+ case TYPE_DATA: return "DATA";
+ case TYPE_RESOURCE: return "RESOURCE";
+ case TYPE_URI: return "URI";
+ default: return "UNKNOWN";
+ }
+ }
+
+ /**
+ * Invokes {@link #loadDrawable(Context)} on the given {@link android.os.Handler Handler}
+ * and then sends <code>andThen</code> to the same Handler when finished.
+ *
+ * @param context {@link android.content.Context Context} in which to load the drawable; see
+ * {@link #loadDrawable(Context)}
+ * @param andThen {@link android.os.Message} to send to its target once the drawable
+ * is available. The {@link android.os.Message#obj obj}
+ * property is populated with the Drawable.
+ */
+ public void loadDrawableAsync(Context context, Message andThen) {
+ if (andThen.getTarget() == null) {
+ throw new IllegalArgumentException("callback message must have a target handler");
+ }
+ new LoadDrawableTask(context, andThen).runAsync();
+ }
+
+ /**
+ * Invokes {@link #loadDrawable(Context)} on a background thread and notifies the <code>
+ * {@link OnDrawableLoadedListener#onDrawableLoaded listener} </code> on the {@code handler}
+ * when finished.
+ *
+ * @param context {@link Context Context} in which to load the drawable; see
+ * {@link #loadDrawable(Context)}
+ * @param listener to be {@link OnDrawableLoadedListener#onDrawableLoaded notified} when
+ * {@link #loadDrawable(Context)} finished
+ * @param handler {@link Handler} on which to notify the {@code listener}
+ */
+ public void loadDrawableAsync(Context context, final OnDrawableLoadedListener listener,
+ Handler handler) {
+ new LoadDrawableTask(context, handler, listener).runAsync();
+ }
+
+ /**
+ * Returns a Drawable that can be used to draw the image inside this Icon, constructing it
+ * if necessary. Depending on the type of image, this may not be something you want to do on
+ * the UI thread, so consider using
+ * {@link #loadDrawableAsync(Context, Message) loadDrawableAsync} instead.
+ *
+ * @param context {@link android.content.Context Context} in which to load the drawable; used
+ * to access {@link android.content.res.Resources Resources}, for example.
+ * @return A fresh instance of a drawable for this image, yours to keep.
+ */
+ public Drawable loadDrawable(Context context) {
+ final Drawable result = loadDrawableInner(context);
+ if (result != null && (mTintList != null || mTintMode != DEFAULT_TINT_MODE)) {
+ result.mutate();
+ result.setTintList(mTintList);
+ result.setTintMode(mTintMode);
+ }
+ return result;
+ }
+
+ /**
+ * Do the heavy lifting of loading the drawable, but stop short of applying any tint.
+ */
+ private Drawable loadDrawableInner(Context context) {
+ switch (mType) {
+ case TYPE_BITMAP:
+ return new BitmapDrawable(context.getResources(), getBitmap());
+ case TYPE_ADAPTIVE_BITMAP:
+ return new AdaptiveIconDrawable(null,
+ new BitmapDrawable(context.getResources(), getBitmap()));
+ case TYPE_RESOURCE:
+ if (getResources() == null) {
+ // figure out where to load resources from
+ String resPackage = getResPackage();
+ if (TextUtils.isEmpty(resPackage)) {
+ // if none is specified, try the given context
+ resPackage = context.getPackageName();
+ }
+ if ("android".equals(resPackage)) {
+ mObj1 = Resources.getSystem();
+ } else {
+ final PackageManager pm = context.getPackageManager();
+ try {
+ ApplicationInfo ai = pm.getApplicationInfo(
+ resPackage, PackageManager.MATCH_UNINSTALLED_PACKAGES);
+ if (ai != null) {
+ mObj1 = pm.getResourcesForApplication(ai);
+ } else {
+ break;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, String.format("Unable to find pkg=%s for icon %s",
+ resPackage, this), e);
+ break;
+ }
+ }
+ }
+ try {
+ return getResources().getDrawable(getResId(), context.getTheme());
+ } catch (RuntimeException e) {
+ Log.e(TAG, String.format("Unable to load resource 0x%08x from pkg=%s",
+ getResId(),
+ getResPackage()),
+ e);
+ }
+ break;
+ case TYPE_DATA:
+ return new BitmapDrawable(context.getResources(),
+ BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength())
+ );
+ case TYPE_URI:
+ final Uri uri = getUri();
+ final String scheme = uri.getScheme();
+ InputStream is = null;
+ if (ContentResolver.SCHEME_CONTENT.equals(scheme)
+ || ContentResolver.SCHEME_FILE.equals(scheme)) {
+ try {
+ is = context.getContentResolver().openInputStream(uri);
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to load image from URI: " + uri, e);
+ }
+ } else {
+ try {
+ is = new FileInputStream(new File(mString1));
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Unable to load image from path: " + uri, e);
+ }
+ }
+ if (is != null) {
+ return new BitmapDrawable(context.getResources(),
+ BitmapFactory.decodeStream(is));
+ }
+ break;
+ }
+ return null;
+ }
+
+ /**
+ * Load the requested resources under the given userId, if the system allows it,
+ * before actually loading the drawable.
+ *
+ * @hide
+ */
+ public Drawable loadDrawableAsUser(Context context, int userId) {
+ if (mType == TYPE_RESOURCE) {
+ String resPackage = getResPackage();
+ if (TextUtils.isEmpty(resPackage)) {
+ resPackage = context.getPackageName();
+ }
+ if (getResources() == null && !(getResPackage().equals("android"))) {
+ final PackageManager pm = context.getPackageManager();
+ try {
+ // assign getResources() as the correct user
+ mObj1 = pm.getResourcesForApplicationAsUser(resPackage, userId);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, String.format("Unable to find pkg=%s user=%d",
+ getResPackage(),
+ userId),
+ e);
+ }
+ }
+ }
+ return loadDrawable(context);
+ }
+
+ /** @hide */
+ public static final int MIN_ASHMEM_ICON_SIZE = 128 * (1 << 10);
+
+ /**
+ * Puts the memory used by this instance into Ashmem memory, if possible.
+ * @hide
+ */
+ public void convertToAshmem() {
+ if ((mType == TYPE_BITMAP || mType == TYPE_ADAPTIVE_BITMAP) &&
+ getBitmap().isMutable() &&
+ getBitmap().getAllocationByteCount() >= MIN_ASHMEM_ICON_SIZE) {
+ setBitmap(getBitmap().createAshmemBitmap());
+ }
+ }
+
+ /**
+ * Writes a serialized version of an Icon to the specified stream.
+ *
+ * @param stream The stream on which to serialize the Icon.
+ * @hide
+ */
+ public void writeToStream(OutputStream stream) throws IOException {
+ DataOutputStream dataStream = new DataOutputStream(stream);
+
+ dataStream.writeInt(VERSION_STREAM_SERIALIZER);
+ dataStream.writeByte(mType);
+
+ switch (mType) {
+ case TYPE_BITMAP:
+ case TYPE_ADAPTIVE_BITMAP:
+ getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream);
+ break;
+ case TYPE_DATA:
+ dataStream.writeInt(getDataLength());
+ dataStream.write(getDataBytes(), getDataOffset(), getDataLength());
+ break;
+ case TYPE_RESOURCE:
+ dataStream.writeUTF(getResPackage());
+ dataStream.writeInt(getResId());
+ break;
+ case TYPE_URI:
+ dataStream.writeUTF(getUriString());
+ break;
+ }
+ }
+
+ private Icon(int mType) {
+ this.mType = mType;
+ }
+
+ /**
+ * Create an Icon from the specified stream.
+ *
+ * @param stream The input stream from which to reconstruct the Icon.
+ * @hide
+ */
+ public static Icon createFromStream(InputStream stream) throws IOException {
+ DataInputStream inputStream = new DataInputStream(stream);
+
+ final int version = inputStream.readInt();
+ if (version >= VERSION_STREAM_SERIALIZER) {
+ final int type = inputStream.readByte();
+ switch (type) {
+ case TYPE_BITMAP:
+ return createWithBitmap(BitmapFactory.decodeStream(inputStream));
+ case TYPE_ADAPTIVE_BITMAP:
+ return createWithAdaptiveBitmap(BitmapFactory.decodeStream(inputStream));
+ case TYPE_DATA:
+ final int length = inputStream.readInt();
+ final byte[] data = new byte[length];
+ inputStream.read(data, 0 /* offset */, length);
+ return createWithData(data, 0 /* offset */, length);
+ case TYPE_RESOURCE:
+ final String packageName = inputStream.readUTF();
+ final int resId = inputStream.readInt();
+ return createWithResource(packageName, resId);
+ case TYPE_URI:
+ final String uriOrPath = inputStream.readUTF();
+ return createWithContentUri(uriOrPath);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Compares if this icon is constructed from the same resources as another icon.
+ * Note that this is an inexpensive operation and doesn't do deep Bitmap equality comparisons.
+ *
+ * @param otherIcon the other icon
+ * @return whether this icon is the same as the another one
+ * @hide
+ */
+ public boolean sameAs(Icon otherIcon) {
+ if (otherIcon == this) {
+ return true;
+ }
+ if (mType != otherIcon.getType()) {
+ return false;
+ }
+ switch (mType) {
+ case TYPE_BITMAP:
+ case TYPE_ADAPTIVE_BITMAP:
+ return getBitmap() == otherIcon.getBitmap();
+ case TYPE_DATA:
+ return getDataLength() == otherIcon.getDataLength()
+ && getDataOffset() == otherIcon.getDataOffset()
+ && Arrays.equals(getDataBytes(), otherIcon.getDataBytes());
+ case TYPE_RESOURCE:
+ return getResId() == otherIcon.getResId()
+ && Objects.equals(getResPackage(), otherIcon.getResPackage());
+ case TYPE_URI:
+ return Objects.equals(getUriString(), otherIcon.getUriString());
+ }
+ return false;
+ }
+
+ /**
+ * Create an Icon pointing to a drawable resource.
+ * @param context The context for the application whose resources should be used to resolve the
+ * given resource ID.
+ * @param resId ID of the drawable resource
+ */
+ public static Icon createWithResource(Context context, @DrawableRes int resId) {
+ if (context == null) {
+ throw new IllegalArgumentException("Context must not be null.");
+ }
+ final Icon rep = new Icon(TYPE_RESOURCE);
+ rep.mInt1 = resId;
+ rep.mString1 = context.getPackageName();
+ return rep;
+ }
+
+ /**
+ * Version of createWithResource that takes Resources. Do not use.
+ * @hide
+ */
+ public static Icon createWithResource(Resources res, @DrawableRes int resId) {
+ if (res == null) {
+ throw new IllegalArgumentException("Resource must not be null.");
+ }
+ final Icon rep = new Icon(TYPE_RESOURCE);
+ rep.mInt1 = resId;
+ rep.mString1 = res.getResourcePackageName(resId);
+ return rep;
+ }
+
+ /**
+ * Create an Icon pointing to a drawable resource.
+ * @param resPackage Name of the package containing the resource in question
+ * @param resId ID of the drawable resource
+ */
+ public static Icon createWithResource(String resPackage, @DrawableRes int resId) {
+ if (resPackage == null) {
+ throw new IllegalArgumentException("Resource package name must not be null.");
+ }
+ final Icon rep = new Icon(TYPE_RESOURCE);
+ rep.mInt1 = resId;
+ rep.mString1 = resPackage;
+ return rep;
+ }
+
+ /**
+ * Create an Icon pointing to a bitmap in memory.
+ * @param bits A valid {@link android.graphics.Bitmap} object
+ */
+ public static Icon createWithBitmap(Bitmap bits) {
+ if (bits == null) {
+ throw new IllegalArgumentException("Bitmap must not be null.");
+ }
+ final Icon rep = new Icon(TYPE_BITMAP);
+ rep.setBitmap(bits);
+ return rep;
+ }
+
+ /**
+ * Create an Icon pointing to a bitmap in memory that follows the icon design guideline defined
+ * by {@link AdaptiveIconDrawable}.
+ * @param bits A valid {@link android.graphics.Bitmap} object
+ */
+ public static Icon createWithAdaptiveBitmap(Bitmap bits) {
+ if (bits == null) {
+ throw new IllegalArgumentException("Bitmap must not be null.");
+ }
+ final Icon rep = new Icon(TYPE_ADAPTIVE_BITMAP);
+ rep.setBitmap(bits);
+ return rep;
+ }
+
+ /**
+ * Create an Icon pointing to a compressed bitmap stored in a byte array.
+ * @param data Byte array storing compressed bitmap data of a type that
+ * {@link android.graphics.BitmapFactory}
+ * can decode (see {@link android.graphics.Bitmap.CompressFormat}).
+ * @param offset Offset into <code>data</code> at which the bitmap data starts
+ * @param length Length of the bitmap data
+ */
+ public static Icon createWithData(byte[] data, int offset, int length) {
+ if (data == null) {
+ throw new IllegalArgumentException("Data must not be null.");
+ }
+ final Icon rep = new Icon(TYPE_DATA);
+ rep.mObj1 = data;
+ rep.mInt1 = length;
+ rep.mInt2 = offset;
+ return rep;
+ }
+
+ /**
+ * Create an Icon pointing to an image file specified by URI.
+ *
+ * @param uri A uri referring to local content:// or file:// image data.
+ */
+ public static Icon createWithContentUri(String uri) {
+ if (uri == null) {
+ throw new IllegalArgumentException("Uri must not be null.");
+ }
+ final Icon rep = new Icon(TYPE_URI);
+ rep.mString1 = uri;
+ return rep;
+ }
+
+ /**
+ * Create an Icon pointing to an image file specified by URI.
+ *
+ * @param uri A uri referring to local content:// or file:// image data.
+ */
+ public static Icon createWithContentUri(Uri uri) {
+ if (uri == null) {
+ throw new IllegalArgumentException("Uri must not be null.");
+ }
+ final Icon rep = new Icon(TYPE_URI);
+ rep.mString1 = uri.toString();
+ return rep;
+ }
+
+ /**
+ * Store a color to use whenever this Icon is drawn.
+ *
+ * @param tint a color, as in {@link Drawable#setTint(int)}
+ * @return this same object, for use in chained construction
+ */
+ public Icon setTint(@ColorInt int tint) {
+ return setTintList(ColorStateList.valueOf(tint));
+ }
+
+ /**
+ * Store a color to use whenever this Icon is drawn.
+ *
+ * @param tintList as in {@link Drawable#setTintList(ColorStateList)}, null to remove tint
+ * @return this same object, for use in chained construction
+ */
+ public Icon setTintList(ColorStateList tintList) {
+ mTintList = tintList;
+ return this;
+ }
+
+ /**
+ * Store a blending mode to use whenever this Icon is drawn.
+ *
+ * @param mode a blending mode, as in {@link Drawable#setTintMode(PorterDuff.Mode)}, may be null
+ * @return this same object, for use in chained construction
+ */
+ public Icon setTintMode(PorterDuff.Mode mode) {
+ mTintMode = mode;
+ return this;
+ }
+
+ /** @hide */
+ public boolean hasTint() {
+ return (mTintList != null) || (mTintMode != DEFAULT_TINT_MODE);
+ }
+
+ /**
+ * Create an Icon pointing to an image file specified by path.
+ *
+ * @param path A path to a file that contains compressed bitmap data of
+ * a type that {@link android.graphics.BitmapFactory} can decode.
+ */
+ public static Icon createWithFilePath(String path) {
+ if (path == null) {
+ throw new IllegalArgumentException("Path must not be null.");
+ }
+ final Icon rep = new Icon(TYPE_URI);
+ rep.mString1 = path;
+ return rep;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType));
+ switch (mType) {
+ case TYPE_BITMAP:
+ case TYPE_ADAPTIVE_BITMAP:
+ sb.append(" size=")
+ .append(getBitmap().getWidth())
+ .append("x")
+ .append(getBitmap().getHeight());
+ break;
+ case TYPE_RESOURCE:
+ sb.append(" pkg=")
+ .append(getResPackage())
+ .append(" id=")
+ .append(String.format("0x%08x", getResId()));
+ break;
+ case TYPE_DATA:
+ sb.append(" len=").append(getDataLength());
+ if (getDataOffset() != 0) {
+ sb.append(" off=").append(getDataOffset());
+ }
+ break;
+ case TYPE_URI:
+ sb.append(" uri=").append(getUriString());
+ break;
+ }
+ if (mTintList != null) {
+ sb.append(" tint=");
+ String sep = "";
+ for (int c : mTintList.getColors()) {
+ sb.append(String.format("%s0x%08x", sep, c));
+ sep = "|";
+ }
+ }
+ if (mTintMode != DEFAULT_TINT_MODE) sb.append(" mode=").append(mTintMode);
+ sb.append(")");
+ return sb.toString();
+ }
+
+ /**
+ * Parcelable interface
+ */
+ public int describeContents() {
+ return (mType == TYPE_BITMAP || mType == TYPE_ADAPTIVE_BITMAP || mType == TYPE_DATA)
+ ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
+ }
+
+ // ===== Parcelable interface ======
+
+ private Icon(Parcel in) {
+ this(in.readInt());
+ switch (mType) {
+ case TYPE_BITMAP:
+ case TYPE_ADAPTIVE_BITMAP:
+ final Bitmap bits = Bitmap.CREATOR.createFromParcel(in);
+ mObj1 = bits;
+ break;
+ case TYPE_RESOURCE:
+ final String pkg = in.readString();
+ final int resId = in.readInt();
+ mString1 = pkg;
+ mInt1 = resId;
+ break;
+ case TYPE_DATA:
+ final int len = in.readInt();
+ final byte[] a = in.readBlob();
+ if (len != a.length) {
+ throw new RuntimeException("internal unparceling error: blob length ("
+ + a.length + ") != expected length (" + len + ")");
+ }
+ mInt1 = len;
+ mObj1 = a;
+ break;
+ case TYPE_URI:
+ final String uri = in.readString();
+ mString1 = uri;
+ break;
+ default:
+ throw new RuntimeException("invalid "
+ + this.getClass().getSimpleName() + " type in parcel: " + mType);
+ }
+ if (in.readInt() == 1) {
+ mTintList = ColorStateList.CREATOR.createFromParcel(in);
+ }
+ mTintMode = PorterDuff.intToMode(in.readInt());
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mType);
+ switch (mType) {
+ case TYPE_BITMAP:
+ case TYPE_ADAPTIVE_BITMAP:
+ final Bitmap bits = getBitmap();
+ getBitmap().writeToParcel(dest, flags);
+ break;
+ case TYPE_RESOURCE:
+ dest.writeString(getResPackage());
+ dest.writeInt(getResId());
+ break;
+ case TYPE_DATA:
+ dest.writeInt(getDataLength());
+ dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength());
+ break;
+ case TYPE_URI:
+ dest.writeString(getUriString());
+ break;
+ }
+ if (mTintList == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mTintList.writeToParcel(dest, flags);
+ }
+ dest.writeInt(PorterDuff.modeToInt(mTintMode));
+ }
+
+ public static final Parcelable.Creator<Icon> CREATOR
+ = new Parcelable.Creator<Icon>() {
+ public Icon createFromParcel(Parcel in) {
+ return new Icon(in);
+ }
+
+ public Icon[] newArray(int size) {
+ return new Icon[size];
+ }
+ };
+
+ /**
+ * Scale down a bitmap to a given max width and max height. The scaling will be done in a uniform way
+ * @param bitmap the bitmap to scale down
+ * @param maxWidth the maximum width allowed
+ * @param maxHeight the maximum height allowed
+ *
+ * @return the scaled bitmap if necessary or the original bitmap if no scaling was needed
+ * @hide
+ */
+ public static Bitmap scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight) {
+ int bitmapWidth = bitmap.getWidth();
+ int bitmapHeight = bitmap.getHeight();
+ if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {
+ float scale = Math.min((float) maxWidth / bitmapWidth,
+ (float) maxHeight / bitmapHeight);
+ bitmap = Bitmap.createScaledBitmap(bitmap, (int) (scale * bitmapWidth),
+ (int) (scale * bitmapHeight), true /* filter */);
+ }
+ return bitmap;
+ }
+
+ /**
+ * Scale down this icon to a given max width and max height.
+ * The scaling will be done in a uniform way and currently only bitmaps are supported.
+ * @param maxWidth the maximum width allowed
+ * @param maxHeight the maximum height allowed
+ *
+ * @hide
+ */
+ public void scaleDownIfNecessary(int maxWidth, int maxHeight) {
+ if (mType != TYPE_BITMAP && mType != TYPE_ADAPTIVE_BITMAP) {
+ return;
+ }
+ Bitmap bitmap = getBitmap();
+ setBitmap(scaleDownIfNecessary(bitmap, maxWidth, maxHeight));
+ }
+
+ /**
+ * Implement this interface to receive a callback when
+ * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync}
+ * is finished and your Drawable is ready.
+ */
+ public interface OnDrawableLoadedListener {
+ void onDrawableLoaded(Drawable d);
+ }
+
+ /**
+ * Wrapper around loadDrawable that does its work on a pooled thread and then
+ * fires back the given (targeted) Message.
+ */
+ private class LoadDrawableTask implements Runnable {
+ final Context mContext;
+ final Message mMessage;
+
+ public LoadDrawableTask(Context context, final Handler handler,
+ final OnDrawableLoadedListener listener) {
+ mContext = context;
+ mMessage = Message.obtain(handler, new Runnable() {
+ @Override
+ public void run() {
+ listener.onDrawableLoaded((Drawable) mMessage.obj);
+ }
+ });
+ }
+
+ public LoadDrawableTask(Context context, Message message) {
+ mContext = context;
+ mMessage = message;
+ }
+
+ @Override
+ public void run() {
+ mMessage.obj = loadDrawable(mContext);
+ mMessage.sendToTarget();
+ }
+
+ public void runAsync() {
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(this);
+ }
+ }
+}
diff --git a/android/graphics/drawable/InsetDrawable.java b/android/graphics/drawable/InsetDrawable.java
new file mode 100644
index 0000000..443aa49
--- /dev/null
+++ b/android/graphics/drawable/InsetDrawable.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2008 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 android.graphics.drawable;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Insets;
+import android.graphics.Outline;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+
+import java.io.IOException;
+
+/**
+ * A Drawable that insets another Drawable by a specified distance or fraction of the content bounds.
+ * This is used when a View needs a background that is smaller than
+ * the View's actual bounds.
+ *
+ * <p>It can be defined in an XML file with the <code><inset></code> element. For more
+ * information, see the guide to <a
+ * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
+ *
+ * @attr ref android.R.styleable#InsetDrawable_visible
+ * @attr ref android.R.styleable#InsetDrawable_drawable
+ * @attr ref android.R.styleable#InsetDrawable_insetLeft
+ * @attr ref android.R.styleable#InsetDrawable_insetRight
+ * @attr ref android.R.styleable#InsetDrawable_insetTop
+ * @attr ref android.R.styleable#InsetDrawable_insetBottom
+ */
+public class InsetDrawable extends DrawableWrapper {
+ private final Rect mTmpRect = new Rect();
+ private final Rect mTmpInsetRect = new Rect();
+
+ private InsetState mState;
+
+ /**
+ * No-arg constructor used by drawable inflation.
+ */
+ InsetDrawable() {
+ this(new InsetState(null, null), null);
+ }
+
+ /**
+ * Creates a new inset drawable with the specified inset.
+ *
+ * @param drawable The drawable to inset.
+ * @param inset Inset in pixels around the drawable.
+ */
+ public InsetDrawable(@Nullable Drawable drawable, int inset) {
+ this(drawable, inset, inset, inset, inset);
+ }
+
+ /**
+ * Creates a new inset drawable with the specified inset.
+ *
+ * @param drawable The drawable to inset.
+ * @param inset Inset in fraction (range: [0, 1)) of the inset content bounds.
+ */
+ public InsetDrawable(@Nullable Drawable drawable, float inset) {
+ this(drawable, inset, inset, inset, inset);
+ }
+
+ /**
+ * Creates a new inset drawable with the specified insets in pixels.
+ *
+ * @param drawable The drawable to inset.
+ * @param insetLeft Left inset in pixels.
+ * @param insetTop Top inset in pixels.
+ * @param insetRight Right inset in pixels.
+ * @param insetBottom Bottom inset in pixels.
+ */
+ public InsetDrawable(@Nullable Drawable drawable, int insetLeft, int insetTop,
+ int insetRight, int insetBottom) {
+ this(new InsetState(null, null), null);
+
+ mState.mInsetLeft = new InsetValue(0f, insetLeft);
+ mState.mInsetTop = new InsetValue(0f, insetTop);
+ mState.mInsetRight = new InsetValue(0f, insetRight);
+ mState.mInsetBottom = new InsetValue(0f, insetBottom);
+
+ setDrawable(drawable);
+ }
+
+ /**
+ * Creates a new inset drawable with the specified insets in fraction of the view bounds.
+ *
+ * @param drawable The drawable to inset.
+ * @param insetLeftFraction Left inset in fraction (range: [0, 1)) of the inset content bounds.
+ * @param insetTopFraction Top inset in fraction (range: [0, 1)) of the inset content bounds.
+ * @param insetRightFraction Right inset in fraction (range: [0, 1)) of the inset content bounds.
+ * @param insetBottomFraction Bottom inset in fraction (range: [0, 1)) of the inset content bounds.
+ */
+ public InsetDrawable(@Nullable Drawable drawable, float insetLeftFraction,
+ float insetTopFraction, float insetRightFraction, float insetBottomFraction) {
+ this(new InsetState(null, null), null);
+
+ mState.mInsetLeft = new InsetValue(insetLeftFraction, 0);
+ mState.mInsetTop = new InsetValue(insetTopFraction, 0);
+ mState.mInsetRight = new InsetValue(insetRightFraction, 0);
+ mState.mInsetBottom = new InsetValue(insetBottomFraction, 0);
+
+ setDrawable(drawable);
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.InsetDrawable);
+
+ // Inflation will advance the XmlPullParser and AttributeSet.
+ super.inflate(r, parser, attrs, theme);
+
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ a.recycle();
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ final InsetState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.InsetDrawable);
+ try {
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ } catch (XmlPullParserException e) {
+ rethrowAsRuntimeException(e);
+ } finally {
+ a.recycle();
+ }
+ }
+ }
+
+ private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
+ // If we're not waiting on a theme, verify required attributes.
+ if (getDrawable() == null && (mState.mThemeAttrs == null
+ || mState.mThemeAttrs[R.styleable.InsetDrawable_drawable] == 0)) {
+ throw new XmlPullParserException(a.getPositionDescription()
+ + ": <inset> tag requires a 'drawable' attribute or "
+ + "child tag defining a drawable");
+ }
+ }
+
+ private void updateStateFromTypedArray(@NonNull TypedArray a) {
+ final InsetState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ // Inset attribute may be overridden by more specific attributes.
+ if (a.hasValue(R.styleable.InsetDrawable_inset)) {
+ final InsetValue inset = getInset(a, R.styleable.InsetDrawable_inset, new InsetValue());
+ state.mInsetLeft = inset;
+ state.mInsetTop = inset;
+ state.mInsetRight = inset;
+ state.mInsetBottom = inset;
+ }
+ state.mInsetLeft = getInset(a, R.styleable.InsetDrawable_insetLeft, state.mInsetLeft);
+ state.mInsetTop = getInset(a, R.styleable.InsetDrawable_insetTop, state.mInsetTop);
+ state.mInsetRight = getInset(a, R.styleable.InsetDrawable_insetRight, state.mInsetRight);
+ state.mInsetBottom = getInset(a, R.styleable.InsetDrawable_insetBottom, state.mInsetBottom);
+ }
+
+ private InsetValue getInset(@NonNull TypedArray a, int index, InsetValue defaultValue) {
+ if (a.hasValue(index)) {
+ TypedValue tv = a.peekValue(index);
+ if (tv.type == TypedValue.TYPE_FRACTION) {
+ float f = tv.getFraction(1.0f, 1.0f);
+ if (f >= 1f) {
+ throw new IllegalStateException("Fraction cannot be larger than 1");
+ }
+ return new InsetValue(f, 0);
+ } else {
+ int dimension = a.getDimensionPixelOffset(index, 0);
+ if (dimension != 0) {
+ return new InsetValue(0, dimension);
+ }
+ }
+ }
+ return defaultValue;
+ }
+
+ private void getInsets(Rect out) {
+ final Rect b = getBounds();
+ out.left = mState.mInsetLeft.getDimension(b.width());
+ out.right = mState.mInsetRight.getDimension(b.width());
+ out.top = mState.mInsetTop.getDimension(b.height());
+ out.bottom = mState.mInsetBottom.getDimension(b.height());
+ }
+
+ @Override
+ public boolean getPadding(Rect padding) {
+ final boolean pad = super.getPadding(padding);
+ getInsets(mTmpInsetRect);
+ padding.left += mTmpInsetRect.left;
+ padding.right += mTmpInsetRect.right;
+ padding.top += mTmpInsetRect.top;
+ padding.bottom += mTmpInsetRect.bottom;
+
+ return pad || (mTmpInsetRect.left | mTmpInsetRect.right
+ | mTmpInsetRect.top | mTmpInsetRect.bottom) != 0;
+ }
+
+ /** @hide */
+ @Override
+ public Insets getOpticalInsets() {
+ final Insets contentInsets = super.getOpticalInsets();
+ getInsets(mTmpInsetRect);
+ return Insets.of(
+ contentInsets.left + mTmpInsetRect.left,
+ contentInsets.top + mTmpInsetRect.top,
+ contentInsets.right + mTmpInsetRect.right,
+ contentInsets.bottom + mTmpInsetRect.bottom);
+ }
+
+ @Override
+ public int getOpacity() {
+ final InsetState state = mState;
+ final int opacity = getDrawable().getOpacity();
+ getInsets(mTmpInsetRect);
+ if (opacity == PixelFormat.OPAQUE &&
+ (mTmpInsetRect.left > 0 || mTmpInsetRect.top > 0 || mTmpInsetRect.right > 0
+ || mTmpInsetRect.bottom > 0)) {
+ return PixelFormat.TRANSLUCENT;
+ }
+ return opacity;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ final Rect r = mTmpRect;
+ r.set(bounds);
+
+ r.left += mState.mInsetLeft.getDimension(bounds.width());
+ r.top += mState.mInsetTop.getDimension(bounds.height());
+ r.right -= mState.mInsetRight.getDimension(bounds.width());
+ r.bottom -= mState.mInsetBottom.getDimension(bounds.height());
+
+ // Apply inset bounds to the wrapped drawable.
+ super.onBoundsChange(r);
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ final int childWidth = getDrawable().getIntrinsicWidth();
+ final float fraction = mState.mInsetLeft.mFraction + mState.mInsetRight.mFraction;
+ if (childWidth < 0 || fraction >= 1) {
+ return -1;
+ }
+ return (int) (childWidth / (1 - fraction)) + mState.mInsetLeft.mDimension
+ + mState.mInsetRight.mDimension;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ final int childHeight = getDrawable().getIntrinsicHeight();
+ final float fraction = mState.mInsetTop.mFraction + mState.mInsetBottom.mFraction;
+ if (childHeight < 0 || fraction >= 1) {
+ return -1;
+ }
+ return (int) (childHeight / (1 - fraction)) + mState.mInsetTop.mDimension
+ + mState.mInsetBottom.mDimension;
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ getDrawable().getOutline(outline);
+ }
+
+ @Override
+ DrawableWrapperState mutateConstantState() {
+ mState = new InsetState(mState, null);
+ return mState;
+ }
+
+ static final class InsetState extends DrawableWrapper.DrawableWrapperState {
+ private int[] mThemeAttrs;
+
+ InsetValue mInsetLeft;
+ InsetValue mInsetTop;
+ InsetValue mInsetRight;
+ InsetValue mInsetBottom;
+
+ InsetState(@Nullable InsetState orig, @Nullable Resources res) {
+ super(orig, res);
+
+ if (orig != null) {
+ mInsetLeft = orig.mInsetLeft.clone();
+ mInsetTop = orig.mInsetTop.clone();
+ mInsetRight = orig.mInsetRight.clone();
+ mInsetBottom = orig.mInsetBottom.clone();
+
+ if (orig.mDensity != mDensity) {
+ applyDensityScaling(orig.mDensity, mDensity);
+ }
+ } else {
+ mInsetLeft = new InsetValue();
+ mInsetTop = new InsetValue();
+ mInsetRight = new InsetValue();
+ mInsetBottom = new InsetValue();
+ }
+ }
+
+ @Override
+ void onDensityChanged(int sourceDensity, int targetDensity) {
+ super.onDensityChanged(sourceDensity, targetDensity);
+
+ applyDensityScaling(sourceDensity, targetDensity);
+ }
+
+ /**
+ * Called when the constant state density changes to scale
+ * density-dependent properties specific to insets.
+ *
+ * @param sourceDensity the previous constant state density
+ * @param targetDensity the new constant state density
+ */
+ private void applyDensityScaling(int sourceDensity, int targetDensity) {
+ mInsetLeft.scaleFromDensity(sourceDensity, targetDensity);
+ mInsetTop.scaleFromDensity(sourceDensity, targetDensity);
+ mInsetRight.scaleFromDensity(sourceDensity, targetDensity);
+ mInsetBottom.scaleFromDensity(sourceDensity, targetDensity);
+ }
+
+ @Override
+ public Drawable newDrawable(@Nullable Resources res) {
+ // If this drawable is being created for a different density,
+ // just create a new constant state and call it a day.
+ final InsetState state;
+ if (res != null) {
+ final int densityDpi = res.getDisplayMetrics().densityDpi;
+ final int density = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
+ if (density != mDensity) {
+ state = new InsetState(this, res);
+ } else {
+ state = this;
+ }
+ } else {
+ state = this;
+ }
+
+ return new InsetDrawable(state, res);
+ }
+ }
+
+ static final class InsetValue implements Cloneable {
+ final float mFraction;
+ int mDimension;
+
+ public InsetValue() {
+ this(0f, 0);
+ }
+
+ public InsetValue(float fraction, int dimension) {
+ mFraction = fraction;
+ mDimension = dimension;
+ }
+ int getDimension(int boundSize) {
+ return (int) (boundSize * mFraction) + mDimension;
+ }
+
+ void scaleFromDensity(int sourceDensity, int targetDensity) {
+ if (mDimension != 0) {
+ mDimension = Bitmap.scaleFromDensity(mDimension, sourceDensity, targetDensity);
+ }
+ }
+
+ @Override
+ public InsetValue clone() {
+ return new InsetValue(mFraction, mDimension);
+ }
+ }
+
+ /**
+ * The one constructor to rule them all. This is called by all public
+ * constructors to set the state and initialize local properties.
+ */
+ private InsetDrawable(@NonNull InsetState state, @Nullable Resources res) {
+ super(state, res);
+
+ mState = state;
+ }
+}
+
diff --git a/android/graphics/drawable/LayerDrawable.java b/android/graphics/drawable/LayerDrawable.java
new file mode 100644
index 0000000..4725c2c
--- /dev/null
+++ b/android/graphics/drawable/LayerDrawable.java
@@ -0,0 +1,2162 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Outline;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.LayoutDirection;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * A Drawable that manages an array of other Drawables. These are drawn in array
+ * order, so the element with the largest index will be drawn on top.
+ * <p>
+ * It can be defined in an XML file with the <code><layer-list></code> element.
+ * Each Drawable in the layer is defined in a nested <code><item></code>.
+ * <p>
+ * For more information, see the guide to
+ * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
+ *
+ * @attr ref android.R.styleable#LayerDrawable_paddingMode
+ * @attr ref android.R.styleable#LayerDrawableItem_left
+ * @attr ref android.R.styleable#LayerDrawableItem_top
+ * @attr ref android.R.styleable#LayerDrawableItem_right
+ * @attr ref android.R.styleable#LayerDrawableItem_bottom
+ * @attr ref android.R.styleable#LayerDrawableItem_start
+ * @attr ref android.R.styleable#LayerDrawableItem_end
+ * @attr ref android.R.styleable#LayerDrawableItem_width
+ * @attr ref android.R.styleable#LayerDrawableItem_height
+ * @attr ref android.R.styleable#LayerDrawableItem_gravity
+ * @attr ref android.R.styleable#LayerDrawableItem_drawable
+ * @attr ref android.R.styleable#LayerDrawableItem_id
+*/
+public class LayerDrawable extends Drawable implements Drawable.Callback {
+ private static final String LOG_TAG = "LayerDrawable";
+
+ /**
+ * Padding mode used to nest each layer inside the padding of the previous
+ * layer.
+ *
+ * @see #setPaddingMode(int)
+ */
+ public static final int PADDING_MODE_NEST = 0;
+
+ /**
+ * Padding mode used to stack each layer directly atop the previous layer.
+ *
+ * @see #setPaddingMode(int)
+ */
+ public static final int PADDING_MODE_STACK = 1;
+
+ /**
+ * Value used for undefined start and end insets.
+ *
+ * @see #getLayerInsetStart(int)
+ * @see #getLayerInsetEnd(int)
+ */
+ public static final int INSET_UNDEFINED = Integer.MIN_VALUE;
+
+ @NonNull
+ LayerState mLayerState;
+
+ private int[] mPaddingL;
+ private int[] mPaddingT;
+ private int[] mPaddingR;
+ private int[] mPaddingB;
+
+ private final Rect mTmpRect = new Rect();
+ private final Rect mTmpOutRect = new Rect();
+ private final Rect mTmpContainer = new Rect();
+ private Rect mHotspotBounds;
+ private boolean mMutated;
+
+ private boolean mSuspendChildInvalidation;
+ private boolean mChildRequestedInvalidation;
+
+ /**
+ * Creates a new layer drawable with the list of specified layers.
+ *
+ * @param layers a list of drawables to use as layers in this new drawable,
+ * must be non-null
+ */
+ public LayerDrawable(@NonNull Drawable[] layers) {
+ this(layers, null);
+ }
+
+ /**
+ * Creates a new layer drawable with the specified list of layers and the
+ * specified constant state.
+ *
+ * @param layers The list of layers to add to this drawable.
+ * @param state The constant drawable state.
+ */
+ LayerDrawable(@NonNull Drawable[] layers, @Nullable LayerState state) {
+ this(state, null);
+
+ if (layers == null) {
+ throw new IllegalArgumentException("layers must be non-null");
+ }
+
+ final int length = layers.length;
+ final ChildDrawable[] r = new ChildDrawable[length];
+ for (int i = 0; i < length; i++) {
+ r[i] = new ChildDrawable(mLayerState.mDensity);
+ r[i].mDrawable = layers[i];
+ layers[i].setCallback(this);
+ mLayerState.mChildrenChangingConfigurations |= layers[i].getChangingConfigurations();
+ }
+ mLayerState.mNumChildren = length;
+ mLayerState.mChildren = r;
+
+ ensurePadding();
+ refreshPadding();
+ }
+
+ LayerDrawable() {
+ this((LayerState) null, null);
+ }
+
+ /**
+ * The one constructor to rule them all. This is called by all public
+ * constructors to set the state and initialize local properties.
+ */
+ LayerDrawable(@Nullable LayerState state, @Nullable Resources res) {
+ mLayerState = createConstantState(state, res);
+ if (mLayerState.mNumChildren > 0) {
+ ensurePadding();
+ refreshPadding();
+ }
+ }
+
+ LayerState createConstantState(@Nullable LayerState state, @Nullable Resources res) {
+ return new LayerState(state, this, res);
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ super.inflate(r, parser, attrs, theme);
+
+ // The density may have changed since the last update. This will
+ // apply scaling to any existing constant state properties.
+ final LayerState state = mLayerState;
+ final int density = Drawable.resolveDensity(r, 0);
+ state.setDensity(density);
+
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.LayerDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+
+ final ChildDrawable[] array = state.mChildren;
+ final int N = state.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final ChildDrawable layer = array[i];
+ layer.setDensity(density);
+ }
+
+ inflateLayers(r, parser, attrs, theme);
+
+ ensurePadding();
+ refreshPadding();
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ final LayerState state = mLayerState;
+ final int density = Drawable.resolveDensity(t.getResources(), 0);
+ state.setDensity(density);
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(
+ state.mThemeAttrs, R.styleable.LayerDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+ }
+
+ final ChildDrawable[] array = state.mChildren;
+ final int N = state.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final ChildDrawable layer = array[i];
+ layer.setDensity(density);
+
+ if (layer.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(
+ layer.mThemeAttrs, R.styleable.LayerDrawableItem);
+ updateLayerFromTypedArray(layer, a);
+ a.recycle();
+ }
+
+ final Drawable d = layer.mDrawable;
+ if (d != null && d.canApplyTheme()) {
+ d.applyTheme(t);
+
+ // Update cached mask of child changing configurations.
+ state.mChildrenChangingConfigurations |= d.getChangingConfigurations();
+ }
+ }
+ }
+
+ /**
+ * Inflates child layers using the specified parser.
+ */
+ private void inflateLayers(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ final LayerState state = mLayerState;
+
+ final int innerDepth = parser.getDepth() + 1;
+ int type;
+ int depth;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (depth > innerDepth || !parser.getName().equals("item")) {
+ continue;
+ }
+
+ final ChildDrawable layer = new ChildDrawable(state.mDensity);
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.LayerDrawableItem);
+ updateLayerFromTypedArray(layer, a);
+ a.recycle();
+
+ // If the layer doesn't have a drawable or unresolved theme
+ // attribute for a drawable, attempt to parse one from the child
+ // element. If multiple child elements exist, we'll only use the
+ // first one.
+ if (layer.mDrawable == null && (layer.mThemeAttrs == null ||
+ layer.mThemeAttrs[R.styleable.LayerDrawableItem_drawable] == 0)) {
+ while ((type = parser.next()) == XmlPullParser.TEXT) {
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException(parser.getPositionDescription()
+ + ": <item> tag requires a 'drawable' attribute or "
+ + "child tag defining a drawable");
+ }
+
+ // We found a child drawable. Take ownership.
+ layer.mDrawable = Drawable.createFromXmlInner(r, parser, attrs, theme);
+ layer.mDrawable.setCallback(this);
+ state.mChildrenChangingConfigurations |=
+ layer.mDrawable.getChangingConfigurations();
+ }
+
+ addLayer(layer);
+ }
+ }
+
+ /**
+ * Initializes the constant state from the values in the typed array.
+ */
+ private void updateStateFromTypedArray(@NonNull TypedArray a) {
+ final LayerState state = mLayerState;
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ final int N = a.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ final int attr = a.getIndex(i);
+ switch (attr) {
+ case R.styleable.LayerDrawable_opacity:
+ state.mOpacityOverride = a.getInt(attr, state.mOpacityOverride);
+ break;
+ case R.styleable.LayerDrawable_paddingTop:
+ state.mPaddingTop = a.getDimensionPixelOffset(attr, state.mPaddingTop);
+ break;
+ case R.styleable.LayerDrawable_paddingBottom:
+ state.mPaddingBottom = a.getDimensionPixelOffset(attr, state.mPaddingBottom);
+ break;
+ case R.styleable.LayerDrawable_paddingLeft:
+ state.mPaddingLeft = a.getDimensionPixelOffset(attr, state.mPaddingLeft);
+ break;
+ case R.styleable.LayerDrawable_paddingRight:
+ state.mPaddingRight = a.getDimensionPixelOffset(attr, state.mPaddingRight);
+ break;
+ case R.styleable.LayerDrawable_paddingStart:
+ state.mPaddingStart = a.getDimensionPixelOffset(attr, state.mPaddingStart);
+ break;
+ case R.styleable.LayerDrawable_paddingEnd:
+ state.mPaddingEnd = a.getDimensionPixelOffset(attr, state.mPaddingEnd);
+ break;
+ case R.styleable.LayerDrawable_autoMirrored:
+ state.mAutoMirrored = a.getBoolean(attr, state.mAutoMirrored);
+ break;
+ case R.styleable.LayerDrawable_paddingMode:
+ state.mPaddingMode = a.getInteger(attr, state.mPaddingMode);
+ break;
+ }
+ }
+ }
+
+ private void updateLayerFromTypedArray(@NonNull ChildDrawable layer, @NonNull TypedArray a) {
+ final LayerState state = mLayerState;
+
+ // Account for any configuration changes.
+ state.mChildrenChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ layer.mThemeAttrs = a.extractThemeAttrs();
+
+ final int N = a.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ final int attr = a.getIndex(i);
+ switch (attr) {
+ case R.styleable.LayerDrawableItem_left:
+ layer.mInsetL = a.getDimensionPixelOffset(attr, layer.mInsetL);
+ break;
+ case R.styleable.LayerDrawableItem_top:
+ layer.mInsetT = a.getDimensionPixelOffset(attr, layer.mInsetT);
+ break;
+ case R.styleable.LayerDrawableItem_right:
+ layer.mInsetR = a.getDimensionPixelOffset(attr, layer.mInsetR);
+ break;
+ case R.styleable.LayerDrawableItem_bottom:
+ layer.mInsetB = a.getDimensionPixelOffset(attr, layer.mInsetB);
+ break;
+ case R.styleable.LayerDrawableItem_start:
+ layer.mInsetS = a.getDimensionPixelOffset(attr, layer.mInsetS);
+ break;
+ case R.styleable.LayerDrawableItem_end:
+ layer.mInsetE = a.getDimensionPixelOffset(attr, layer.mInsetE);
+ break;
+ case R.styleable.LayerDrawableItem_width:
+ layer.mWidth = a.getDimensionPixelSize(attr, layer.mWidth);
+ break;
+ case R.styleable.LayerDrawableItem_height:
+ layer.mHeight = a.getDimensionPixelSize(attr, layer.mHeight);
+ break;
+ case R.styleable.LayerDrawableItem_gravity:
+ layer.mGravity = a.getInteger(attr, layer.mGravity);
+ break;
+ case R.styleable.LayerDrawableItem_id:
+ layer.mId = a.getResourceId(attr, layer.mId);
+ break;
+ }
+ }
+
+ final Drawable dr = a.getDrawable(R.styleable.LayerDrawableItem_drawable);
+ if (dr != null) {
+ if (layer.mDrawable != null) {
+ // It's possible that a drawable was already set, in which case
+ // we should clear the callback. We may have also integrated the
+ // drawable's changing configurations, but we don't have enough
+ // information to revert that change.
+ layer.mDrawable.setCallback(null);
+ }
+
+ // Take ownership of the new drawable.
+ layer.mDrawable = dr;
+ layer.mDrawable.setCallback(this);
+ state.mChildrenChangingConfigurations |=
+ layer.mDrawable.getChangingConfigurations();
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mLayerState.canApplyTheme() || super.canApplyTheme();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean isProjected() {
+ if (super.isProjected()) {
+ return true;
+ }
+
+ final ChildDrawable[] layers = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ if (layers[i].mDrawable.isProjected()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds a new layer at the end of list of layers and returns its index.
+ *
+ * @param layer The layer to add.
+ * @return The index of the layer.
+ */
+ int addLayer(@NonNull ChildDrawable layer) {
+ final LayerState st = mLayerState;
+ final int N = st.mChildren != null ? st.mChildren.length : 0;
+ final int i = st.mNumChildren;
+ if (i >= N) {
+ final ChildDrawable[] nu = new ChildDrawable[N + 10];
+ if (i > 0) {
+ System.arraycopy(st.mChildren, 0, nu, 0, i);
+ }
+
+ st.mChildren = nu;
+ }
+
+ st.mChildren[i] = layer;
+ st.mNumChildren++;
+ st.invalidateCache();
+ return i;
+ }
+
+ /**
+ * Add a new layer to this drawable. The new layer is identified by an id.
+ *
+ * @param dr The drawable to add as a layer.
+ * @param themeAttrs Theme attributes extracted from the layer.
+ * @param id The id of the new layer.
+ * @param left The left padding of the new layer.
+ * @param top The top padding of the new layer.
+ * @param right The right padding of the new layer.
+ * @param bottom The bottom padding of the new layer.
+ */
+ ChildDrawable addLayer(Drawable dr, int[] themeAttrs, int id,
+ int left, int top, int right, int bottom) {
+ final ChildDrawable childDrawable = createLayer(dr);
+ childDrawable.mId = id;
+ childDrawable.mThemeAttrs = themeAttrs;
+ childDrawable.mDrawable.setAutoMirrored(isAutoMirrored());
+ childDrawable.mInsetL = left;
+ childDrawable.mInsetT = top;
+ childDrawable.mInsetR = right;
+ childDrawable.mInsetB = bottom;
+
+ addLayer(childDrawable);
+
+ mLayerState.mChildrenChangingConfigurations |= dr.getChangingConfigurations();
+ dr.setCallback(this);
+
+ return childDrawable;
+ }
+
+ private ChildDrawable createLayer(Drawable dr) {
+ final ChildDrawable layer = new ChildDrawable(mLayerState.mDensity);
+ layer.mDrawable = dr;
+ return layer;
+ }
+
+ /**
+ * Adds a new layer containing the specified {@code drawable} to the end of
+ * the layer list and returns its index.
+ *
+ * @param dr The drawable to add as a new layer.
+ * @return The index of the new layer.
+ */
+ public int addLayer(Drawable dr) {
+ final ChildDrawable layer = createLayer(dr);
+ final int index = addLayer(layer);
+ ensurePadding();
+ refreshChildPadding(index, layer);
+ return index;
+ }
+
+ /**
+ * Looks for a layer with the given ID and returns its {@link Drawable}.
+ * <p>
+ * If multiple layers are found for the given ID, returns the
+ * {@link Drawable} for the matching layer at the highest index.
+ *
+ * @param id The layer ID to search for.
+ * @return The {@link Drawable} for the highest-indexed layer that has the
+ * given ID, or null if not found.
+ */
+ public Drawable findDrawableByLayerId(int id) {
+ final ChildDrawable[] layers = mLayerState.mChildren;
+ for (int i = mLayerState.mNumChildren - 1; i >= 0; i--) {
+ if (layers[i].mId == id) {
+ return layers[i].mDrawable;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the ID of a layer.
+ *
+ * @param index The index of the layer to modify, must be in the range
+ * {@code 0...getNumberOfLayers()-1}.
+ * @param id The id to assign to the layer.
+ *
+ * @see #getId(int)
+ * @attr ref android.R.styleable#LayerDrawableItem_id
+ */
+ public void setId(int index, int id) {
+ mLayerState.mChildren[index].mId = id;
+ }
+
+ /**
+ * Returns the ID of the specified layer.
+ *
+ * @param index The index of the layer, must be in the range
+ * {@code 0...getNumberOfLayers()-1}.
+ * @return The id of the layer or {@link android.view.View#NO_ID} if the
+ * layer has no id.
+ *
+ * @see #setId(int, int)
+ * @attr ref android.R.styleable#LayerDrawableItem_id
+ */
+ public int getId(int index) {
+ if (index >= mLayerState.mNumChildren) {
+ throw new IndexOutOfBoundsException();
+ }
+ return mLayerState.mChildren[index].mId;
+ }
+
+ /**
+ * Returns the number of layers contained within this layer drawable.
+ *
+ * @return The number of layers.
+ */
+ public int getNumberOfLayers() {
+ return mLayerState.mNumChildren;
+ }
+
+ /**
+ * Replaces the {@link Drawable} for the layer with the given id.
+ *
+ * @param id The layer ID to search for.
+ * @param drawable The replacement {@link Drawable}.
+ * @return Whether the {@link Drawable} was replaced (could return false if
+ * the id was not found).
+ */
+ public boolean setDrawableByLayerId(int id, Drawable drawable) {
+ final int index = findIndexByLayerId(id);
+ if (index < 0) {
+ return false;
+ }
+
+ setDrawable(index, drawable);
+ return true;
+ }
+
+ /**
+ * Returns the layer with the specified {@code id}.
+ * <p>
+ * If multiple layers have the same ID, returns the layer with the lowest
+ * index.
+ *
+ * @param id The ID of the layer to return.
+ * @return The index of the layer with the specified ID.
+ */
+ public int findIndexByLayerId(int id) {
+ final ChildDrawable[] layers = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final ChildDrawable childDrawable = layers[i];
+ if (childDrawable.mId == id) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Sets the drawable for the layer at the specified index.
+ *
+ * @param index The index of the layer to modify, must be in the range
+ * {@code 0...getNumberOfLayers()-1}.
+ * @param drawable The drawable to set for the layer.
+ *
+ * @see #getDrawable(int)
+ * @attr ref android.R.styleable#LayerDrawableItem_drawable
+ */
+ public void setDrawable(int index, Drawable drawable) {
+ if (index >= mLayerState.mNumChildren) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ final ChildDrawable[] layers = mLayerState.mChildren;
+ final ChildDrawable childDrawable = layers[index];
+ if (childDrawable.mDrawable != null) {
+ if (drawable != null) {
+ final Rect bounds = childDrawable.mDrawable.getBounds();
+ drawable.setBounds(bounds);
+ }
+
+ childDrawable.mDrawable.setCallback(null);
+ }
+
+ if (drawable != null) {
+ drawable.setCallback(this);
+ }
+
+ childDrawable.mDrawable = drawable;
+ mLayerState.invalidateCache();
+
+ refreshChildPadding(index, childDrawable);
+ }
+
+ /**
+ * Returns the drawable for the layer at the specified index.
+ *
+ * @param index The index of the layer, must be in the range
+ * {@code 0...getNumberOfLayers()-1}.
+ * @return The {@link Drawable} at the specified layer index.
+ *
+ * @see #setDrawable(int, Drawable)
+ * @attr ref android.R.styleable#LayerDrawableItem_drawable
+ */
+ public Drawable getDrawable(int index) {
+ if (index >= mLayerState.mNumChildren) {
+ throw new IndexOutOfBoundsException();
+ }
+ return mLayerState.mChildren[index].mDrawable;
+ }
+
+ /**
+ * Sets an explicit size for the specified layer.
+ * <p>
+ * <strong>Note:</strong> Setting an explicit layer size changes the
+ * default layer gravity behavior. See {@link #setLayerGravity(int, int)}
+ * for more information.
+ *
+ * @param index the index of the layer to adjust
+ * @param w width in pixels, or -1 to use the intrinsic width
+ * @param h height in pixels, or -1 to use the intrinsic height
+ * @see #getLayerWidth(int)
+ * @see #getLayerHeight(int)
+ * @attr ref android.R.styleable#LayerDrawableItem_width
+ * @attr ref android.R.styleable#LayerDrawableItem_height
+ */
+ public void setLayerSize(int index, int w, int h) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mWidth = w;
+ childDrawable.mHeight = h;
+ }
+
+ /**
+ * @param index the index of the layer to adjust
+ * @param w width in pixels, or -1 to use the intrinsic width
+ * @attr ref android.R.styleable#LayerDrawableItem_width
+ */
+ public void setLayerWidth(int index, int w) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mWidth = w;
+ }
+
+ /**
+ * @param index the index of the drawable to adjust
+ * @return the explicit width of the layer, or -1 if not specified
+ * @see #setLayerSize(int, int, int)
+ * @attr ref android.R.styleable#LayerDrawableItem_width
+ */
+ public int getLayerWidth(int index) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ return childDrawable.mWidth;
+ }
+
+ /**
+ * @param index the index of the layer to adjust
+ * @param h height in pixels, or -1 to use the intrinsic height
+ * @attr ref android.R.styleable#LayerDrawableItem_height
+ */
+ public void setLayerHeight(int index, int h) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mHeight = h;
+ }
+
+ /**
+ * @param index the index of the drawable to adjust
+ * @return the explicit height of the layer, or -1 if not specified
+ * @see #setLayerSize(int, int, int)
+ * @attr ref android.R.styleable#LayerDrawableItem_height
+ */
+ public int getLayerHeight(int index) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ return childDrawable.mHeight;
+ }
+
+ /**
+ * Sets the gravity used to position or stretch the specified layer within
+ * its container. Gravity is applied after any layer insets (see
+ * {@link #setLayerInset(int, int, int, int, int)}) or padding (see
+ * {@link #setPaddingMode(int)}).
+ * <p>
+ * If gravity is specified as {@link Gravity#NO_GRAVITY}, the default
+ * behavior depends on whether an explicit width or height has been set
+ * (see {@link #setLayerSize(int, int, int)}), If a dimension is not set,
+ * gravity in that direction defaults to {@link Gravity#FILL_HORIZONTAL} or
+ * {@link Gravity#FILL_VERTICAL}; otherwise, gravity in that direction
+ * defaults to {@link Gravity#LEFT} or {@link Gravity#TOP}.
+ *
+ * @param index the index of the drawable to adjust
+ * @param gravity the gravity to set for the layer
+ *
+ * @see #getLayerGravity(int)
+ * @attr ref android.R.styleable#LayerDrawableItem_gravity
+ */
+ public void setLayerGravity(int index, int gravity) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mGravity = gravity;
+ }
+
+ /**
+ * @param index the index of the layer
+ * @return the gravity used to position or stretch the specified layer
+ * within its container
+ *
+ * @see #setLayerGravity(int, int)
+ * @attr ref android.R.styleable#LayerDrawableItem_gravity
+ */
+ public int getLayerGravity(int index) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ return childDrawable.mGravity;
+ }
+
+ /**
+ * Specifies the insets in pixels for the drawable at the specified index.
+ *
+ * @param index the index of the drawable to adjust
+ * @param l number of pixels to add to the left bound
+ * @param t number of pixels to add to the top bound
+ * @param r number of pixels to subtract from the right bound
+ * @param b number of pixels to subtract from the bottom bound
+ *
+ * @attr ref android.R.styleable#LayerDrawableItem_left
+ * @attr ref android.R.styleable#LayerDrawableItem_top
+ * @attr ref android.R.styleable#LayerDrawableItem_right
+ * @attr ref android.R.styleable#LayerDrawableItem_bottom
+ */
+ public void setLayerInset(int index, int l, int t, int r, int b) {
+ setLayerInsetInternal(index, l, t, r, b, INSET_UNDEFINED, INSET_UNDEFINED);
+ }
+
+ /**
+ * Specifies the relative insets in pixels for the drawable at the
+ * specified index.
+ *
+ * @param index the index of the layer to adjust
+ * @param s number of pixels to inset from the start bound
+ * @param t number of pixels to inset from the top bound
+ * @param e number of pixels to inset from the end bound
+ * @param b number of pixels to inset from the bottom bound
+ *
+ * @attr ref android.R.styleable#LayerDrawableItem_start
+ * @attr ref android.R.styleable#LayerDrawableItem_top
+ * @attr ref android.R.styleable#LayerDrawableItem_end
+ * @attr ref android.R.styleable#LayerDrawableItem_bottom
+ */
+ public void setLayerInsetRelative(int index, int s, int t, int e, int b) {
+ setLayerInsetInternal(index, 0, t, 0, b, s, e);
+ }
+
+ /**
+ * @param index the index of the layer to adjust
+ * @param l number of pixels to inset from the left bound
+ * @attr ref android.R.styleable#LayerDrawableItem_left
+ */
+ public void setLayerInsetLeft(int index, int l) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mInsetL = l;
+ }
+
+ /**
+ * @param index the index of the layer
+ * @return number of pixels to inset from the left bound
+ * @attr ref android.R.styleable#LayerDrawableItem_left
+ */
+ public int getLayerInsetLeft(int index) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ return childDrawable.mInsetL;
+ }
+
+ /**
+ * @param index the index of the layer to adjust
+ * @param r number of pixels to inset from the right bound
+ * @attr ref android.R.styleable#LayerDrawableItem_right
+ */
+ public void setLayerInsetRight(int index, int r) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mInsetR = r;
+ }
+
+ /**
+ * @param index the index of the layer
+ * @return number of pixels to inset from the right bound
+ * @attr ref android.R.styleable#LayerDrawableItem_right
+ */
+ public int getLayerInsetRight(int index) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ return childDrawable.mInsetR;
+ }
+
+ /**
+ * @param index the index of the layer to adjust
+ * @param t number of pixels to inset from the top bound
+ * @attr ref android.R.styleable#LayerDrawableItem_top
+ */
+ public void setLayerInsetTop(int index, int t) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mInsetT = t;
+ }
+
+ /**
+ * @param index the index of the layer
+ * @return number of pixels to inset from the top bound
+ * @attr ref android.R.styleable#LayerDrawableItem_top
+ */
+ public int getLayerInsetTop(int index) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ return childDrawable.mInsetT;
+ }
+
+ /**
+ * @param index the index of the layer to adjust
+ * @param b number of pixels to inset from the bottom bound
+ * @attr ref android.R.styleable#LayerDrawableItem_bottom
+ */
+ public void setLayerInsetBottom(int index, int b) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mInsetB = b;
+ }
+
+ /**
+ * @param index the index of the layer
+ * @return number of pixels to inset from the bottom bound
+ * @attr ref android.R.styleable#LayerDrawableItem_bottom
+ */
+ public int getLayerInsetBottom(int index) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ return childDrawable.mInsetB;
+ }
+
+ /**
+ * @param index the index of the layer to adjust
+ * @param s number of pixels to inset from the start bound
+ * @attr ref android.R.styleable#LayerDrawableItem_start
+ */
+ public void setLayerInsetStart(int index, int s) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mInsetS = s;
+ }
+
+ /**
+ * @param index the index of the layer
+ * @return the number of pixels to inset from the start bound, or
+ * {@link #INSET_UNDEFINED} if not specified
+ * @attr ref android.R.styleable#LayerDrawableItem_start
+ */
+ public int getLayerInsetStart(int index) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ return childDrawable.mInsetS;
+ }
+
+ /**
+ * @param index the index of the layer to adjust
+ * @param e number of pixels to inset from the end bound, or
+ * {@link #INSET_UNDEFINED} if not specified
+ * @attr ref android.R.styleable#LayerDrawableItem_end
+ */
+ public void setLayerInsetEnd(int index, int e) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mInsetE = e;
+ }
+
+ /**
+ * @param index the index of the layer
+ * @return number of pixels to inset from the end bound
+ * @attr ref android.R.styleable#LayerDrawableItem_end
+ */
+ public int getLayerInsetEnd(int index) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ return childDrawable.mInsetE;
+ }
+
+ private void setLayerInsetInternal(int index, int l, int t, int r, int b, int s, int e) {
+ final ChildDrawable childDrawable = mLayerState.mChildren[index];
+ childDrawable.mInsetL = l;
+ childDrawable.mInsetT = t;
+ childDrawable.mInsetR = r;
+ childDrawable.mInsetB = b;
+ childDrawable.mInsetS = s;
+ childDrawable.mInsetE = e;
+ }
+
+ /**
+ * Specifies how layer padding should affect the bounds of subsequent
+ * layers. The default value is {@link #PADDING_MODE_NEST}.
+ *
+ * @param mode padding mode, one of:
+ * <ul>
+ * <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
+ * padding of the previous layer
+ * <li>{@link #PADDING_MODE_STACK} to stack each layer directly
+ * atop the previous layer
+ * </ul>
+ *
+ * @see #getPaddingMode()
+ * @attr ref android.R.styleable#LayerDrawable_paddingMode
+ */
+ public void setPaddingMode(int mode) {
+ if (mLayerState.mPaddingMode != mode) {
+ mLayerState.mPaddingMode = mode;
+ }
+ }
+
+ /**
+ * @return the current padding mode
+ *
+ * @see #setPaddingMode(int)
+ * @attr ref android.R.styleable#LayerDrawable_paddingMode
+ */
+ public int getPaddingMode() {
+ return mLayerState.mPaddingMode;
+ }
+
+ /**
+ * Temporarily suspends child invalidation.
+ *
+ * @see #resumeChildInvalidation()
+ */
+ private void suspendChildInvalidation() {
+ mSuspendChildInvalidation = true;
+ }
+
+ /**
+ * Resumes child invalidation after suspension, immediately performing an
+ * invalidation if one was requested by a child during suspension.
+ *
+ * @see #suspendChildInvalidation()
+ */
+ private void resumeChildInvalidation() {
+ mSuspendChildInvalidation = false;
+
+ if (mChildRequestedInvalidation) {
+ mChildRequestedInvalidation = false;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable who) {
+ if (mSuspendChildInvalidation) {
+ mChildRequestedInvalidation = true;
+ } else {
+ // This may have been called as the result of a tint changing, in
+ // which case we may need to refresh the cached statefulness or
+ // opacity.
+ mLayerState.invalidateCache();
+
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+ scheduleSelf(what, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+ unscheduleSelf(what);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.draw(canvas);
+ }
+ }
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations() | mLayerState.getChangingConfigurations();
+ }
+
+ @Override
+ public boolean getPadding(Rect padding) {
+ final LayerState layerState = mLayerState;
+ if (layerState.mPaddingMode == PADDING_MODE_NEST) {
+ computeNestedPadding(padding);
+ } else {
+ computeStackedPadding(padding);
+ }
+
+ final int paddingT = layerState.mPaddingTop;
+ final int paddingB = layerState.mPaddingBottom;
+
+ // Resolve padding for RTL. Relative padding overrides absolute
+ // padding.
+ final boolean isLayoutRtl = getLayoutDirection() == LayoutDirection.RTL;
+ final int paddingRtlL = isLayoutRtl ? layerState.mPaddingEnd : layerState.mPaddingStart;
+ final int paddingRtlR = isLayoutRtl ? layerState.mPaddingStart : layerState.mPaddingEnd;
+ final int paddingL = paddingRtlL >= 0 ? paddingRtlL : layerState.mPaddingLeft;
+ final int paddingR = paddingRtlR >= 0 ? paddingRtlR : layerState.mPaddingRight;
+
+ // If padding was explicitly specified (e.g. not -1) then override the
+ // computed padding in that dimension.
+ if (paddingL >= 0) {
+ padding.left = paddingL;
+ }
+
+ if (paddingT >= 0) {
+ padding.top = paddingT;
+ }
+
+ if (paddingR >= 0) {
+ padding.right = paddingR;
+ }
+
+ if (paddingB >= 0) {
+ padding.bottom = paddingB;
+ }
+
+ return padding.left != 0 || padding.top != 0 || padding.right != 0 || padding.bottom != 0;
+ }
+
+ /**
+ * Sets the absolute padding.
+ * <p>
+ * If padding in a dimension is specified as {@code -1}, the resolved
+ * padding will use the value computed according to the padding mode (see
+ * {@link #setPaddingMode(int)}).
+ * <p>
+ * Calling this method clears any relative padding values previously set
+ * using {@link #setPaddingRelative(int, int, int, int)}.
+ *
+ * @param left the left padding in pixels, or -1 to use computed padding
+ * @param top the top padding in pixels, or -1 to use computed padding
+ * @param right the right padding in pixels, or -1 to use computed padding
+ * @param bottom the bottom padding in pixels, or -1 to use computed
+ * padding
+ * @attr ref android.R.styleable#LayerDrawable_paddingLeft
+ * @attr ref android.R.styleable#LayerDrawable_paddingTop
+ * @attr ref android.R.styleable#LayerDrawable_paddingRight
+ * @attr ref android.R.styleable#LayerDrawable_paddingBottom
+ * @see #setPaddingRelative(int, int, int, int)
+ */
+ public void setPadding(int left, int top, int right, int bottom) {
+ final LayerState layerState = mLayerState;
+ layerState.mPaddingLeft = left;
+ layerState.mPaddingTop = top;
+ layerState.mPaddingRight = right;
+ layerState.mPaddingBottom = bottom;
+
+ // Clear relative padding values.
+ layerState.mPaddingStart = -1;
+ layerState.mPaddingEnd = -1;
+ }
+
+ /**
+ * Sets the relative padding.
+ * <p>
+ * If padding in a dimension is specified as {@code -1}, the resolved
+ * padding will use the value computed according to the padding mode (see
+ * {@link #setPaddingMode(int)}).
+ * <p>
+ * Calling this method clears any absolute padding values previously set
+ * using {@link #setPadding(int, int, int, int)}.
+ *
+ * @param start the start padding in pixels, or -1 to use computed padding
+ * @param top the top padding in pixels, or -1 to use computed padding
+ * @param end the end padding in pixels, or -1 to use computed padding
+ * @param bottom the bottom padding in pixels, or -1 to use computed
+ * padding
+ * @attr ref android.R.styleable#LayerDrawable_paddingStart
+ * @attr ref android.R.styleable#LayerDrawable_paddingTop
+ * @attr ref android.R.styleable#LayerDrawable_paddingEnd
+ * @attr ref android.R.styleable#LayerDrawable_paddingBottom
+ * @see #setPadding(int, int, int, int)
+ */
+ public void setPaddingRelative(int start, int top, int end, int bottom) {
+ final LayerState layerState = mLayerState;
+ layerState.mPaddingStart = start;
+ layerState.mPaddingTop = top;
+ layerState.mPaddingEnd = end;
+ layerState.mPaddingBottom = bottom;
+
+ // Clear absolute padding values.
+ layerState.mPaddingLeft = -1;
+ layerState.mPaddingRight = -1;
+ }
+
+ /**
+ * Returns the left padding in pixels.
+ * <p>
+ * A return value of {@code -1} means there is no explicit padding set for
+ * this dimension. As a result, the value for this dimension returned by
+ * {@link #getPadding(Rect)} will be computed from the child layers
+ * according to the padding mode (see {@link #getPaddingMode()}.
+ *
+ * @return the left padding in pixels, or -1 if not explicitly specified
+ * @see #setPadding(int, int, int, int)
+ * @see #getPadding(Rect)
+ */
+ public int getLeftPadding() {
+ return mLayerState.mPaddingLeft;
+ }
+
+ /**
+ * Returns the right padding in pixels.
+ * <p>
+ * A return value of {@code -1} means there is no explicit padding set for
+ * this dimension. As a result, the value for this dimension returned by
+ * {@link #getPadding(Rect)} will be computed from the child layers
+ * according to the padding mode (see {@link #getPaddingMode()}.
+ *
+ * @return the right padding in pixels, or -1 if not explicitly specified
+ * @see #setPadding(int, int, int, int)
+ * @see #getPadding(Rect)
+ */
+ public int getRightPadding() {
+ return mLayerState.mPaddingRight;
+ }
+
+ /**
+ * Returns the start padding in pixels.
+ * <p>
+ * A return value of {@code -1} means there is no explicit padding set for
+ * this dimension. As a result, the value for this dimension returned by
+ * {@link #getPadding(Rect)} will be computed from the child layers
+ * according to the padding mode (see {@link #getPaddingMode()}.
+ *
+ * @return the start padding in pixels, or -1 if not explicitly specified
+ * @see #setPaddingRelative(int, int, int, int)
+ * @see #getPadding(Rect)
+ */
+ public int getStartPadding() {
+ return mLayerState.mPaddingStart;
+ }
+
+ /**
+ * Returns the end padding in pixels.
+ * <p>
+ * A return value of {@code -1} means there is no explicit padding set for
+ * this dimension. As a result, the value for this dimension returned by
+ * {@link #getPadding(Rect)} will be computed from the child layers
+ * according to the padding mode (see {@link #getPaddingMode()}.
+ *
+ * @return the end padding in pixels, or -1 if not explicitly specified
+ * @see #setPaddingRelative(int, int, int, int)
+ * @see #getPadding(Rect)
+ */
+ public int getEndPadding() {
+ return mLayerState.mPaddingEnd;
+ }
+
+ /**
+ * Returns the top padding in pixels.
+ * <p>
+ * A return value of {@code -1} means there is no explicit padding set for
+ * this dimension. As a result, the value for this dimension returned by
+ * {@link #getPadding(Rect)} will be computed from the child layers
+ * according to the padding mode (see {@link #getPaddingMode()}.
+ *
+ * @return the top padding in pixels, or -1 if not explicitly specified
+ * @see #setPadding(int, int, int, int)
+ * @see #setPaddingRelative(int, int, int, int)
+ * @see #getPadding(Rect)
+ */
+ public int getTopPadding() {
+ return mLayerState.mPaddingTop;
+ }
+
+ /**
+ * Returns the bottom padding in pixels.
+ * <p>
+ * A return value of {@code -1} means there is no explicit padding set for
+ * this dimension. As a result, the value for this dimension returned by
+ * {@link #getPadding(Rect)} will be computed from the child layers
+ * according to the padding mode (see {@link #getPaddingMode()}.
+ *
+ * @return the bottom padding in pixels, or -1 if not explicitly specified
+ * @see #setPadding(int, int, int, int)
+ * @see #setPaddingRelative(int, int, int, int)
+ * @see #getPadding(Rect)
+ */
+ public int getBottomPadding() {
+ return mLayerState.mPaddingBottom;
+ }
+
+ private void computeNestedPadding(Rect padding) {
+ padding.left = 0;
+ padding.top = 0;
+ padding.right = 0;
+ padding.bottom = 0;
+
+ // Add all the padding.
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ refreshChildPadding(i, array[i]);
+
+ padding.left += mPaddingL[i];
+ padding.top += mPaddingT[i];
+ padding.right += mPaddingR[i];
+ padding.bottom += mPaddingB[i];
+ }
+ }
+
+ private void computeStackedPadding(Rect padding) {
+ padding.left = 0;
+ padding.top = 0;
+ padding.right = 0;
+ padding.bottom = 0;
+
+ // Take the max padding.
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ refreshChildPadding(i, array[i]);
+
+ padding.left = Math.max(padding.left, mPaddingL[i]);
+ padding.top = Math.max(padding.top, mPaddingT[i]);
+ padding.right = Math.max(padding.right, mPaddingR[i]);
+ padding.bottom = Math.max(padding.bottom, mPaddingB[i]);
+ }
+ }
+
+ /**
+ * Populates <code>outline</code> with the first available (non-empty) layer outline.
+ *
+ * @param outline Outline in which to place the first available layer outline
+ */
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.getOutline(outline);
+ if (!outline.isEmpty()) {
+ return;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void setHotspot(float x, float y) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setHotspot(x, y);
+ }
+ }
+ }
+
+ @Override
+ public void setHotspotBounds(int left, int top, int right, int bottom) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setHotspotBounds(left, top, right, bottom);
+ }
+ }
+
+ if (mHotspotBounds == null) {
+ mHotspotBounds = new Rect(left, top, right, bottom);
+ } else {
+ mHotspotBounds.set(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public void getHotspotBounds(Rect outRect) {
+ if (mHotspotBounds != null) {
+ outRect.set(mHotspotBounds);
+ } else {
+ super.getHotspotBounds(outRect);
+ }
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ final boolean changed = super.setVisible(visible, restart);
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setVisible(visible, restart);
+ }
+ }
+
+ return changed;
+ }
+
+ @Override
+ public void setDither(boolean dither) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setDither(dither);
+ }
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setAlpha(alpha);
+ }
+ }
+ }
+
+ @Override
+ public int getAlpha() {
+ final Drawable dr = getFirstNonNullDrawable();
+ if (dr != null) {
+ return dr.getAlpha();
+ } else {
+ return super.getAlpha();
+ }
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setColorFilter(colorFilter);
+ }
+ }
+ }
+
+ @Override
+ public void setTintList(ColorStateList tint) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setTintList(tint);
+ }
+ }
+ }
+
+ @Override
+ public void setTintMode(Mode tintMode) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setTintMode(tintMode);
+ }
+ }
+ }
+
+ private Drawable getFirstNonNullDrawable() {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ return dr;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the opacity of this drawable directly instead of collecting the
+ * states from the layers.
+ *
+ * @param opacity The opacity to use, or {@link PixelFormat#UNKNOWN
+ * PixelFormat.UNKNOWN} for the default behavior
+ * @see PixelFormat#UNKNOWN
+ * @see PixelFormat#TRANSLUCENT
+ * @see PixelFormat#TRANSPARENT
+ * @see PixelFormat#OPAQUE
+ */
+ public void setOpacity(int opacity) {
+ mLayerState.mOpacityOverride = opacity;
+ }
+
+ @Override
+ public int getOpacity() {
+ if (mLayerState.mOpacityOverride != PixelFormat.UNKNOWN) {
+ return mLayerState.mOpacityOverride;
+ }
+ return mLayerState.getOpacity();
+ }
+
+ @Override
+ public void setAutoMirrored(boolean mirrored) {
+ mLayerState.mAutoMirrored = mirrored;
+
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.setAutoMirrored(mirrored);
+ }
+ }
+ }
+
+ @Override
+ public boolean isAutoMirrored() {
+ return mLayerState.mAutoMirrored;
+ }
+
+ @Override
+ public void jumpToCurrentState() {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.jumpToCurrentState();
+ }
+ }
+ }
+
+ @Override
+ public boolean isStateful() {
+ return mLayerState.isStateful();
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return mLayerState.hasFocusStateSpecified();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ boolean changed = false;
+
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null && dr.isStateful() && dr.setState(state)) {
+ refreshChildPadding(i, array[i]);
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ updateLayerBounds(getBounds());
+ }
+
+ return changed;
+ }
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ boolean changed = false;
+
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null && dr.setLevel(level)) {
+ refreshChildPadding(i, array[i]);
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ updateLayerBounds(getBounds());
+ }
+
+ return changed;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ updateLayerBounds(bounds);
+ }
+
+ private void updateLayerBounds(Rect bounds) {
+ try {
+ suspendChildInvalidation();
+ updateLayerBoundsInternal(bounds);
+ } finally {
+ resumeChildInvalidation();
+ }
+ }
+
+ private void updateLayerBoundsInternal(Rect bounds) {
+ int paddingL = 0;
+ int paddingT = 0;
+ int paddingR = 0;
+ int paddingB = 0;
+
+ final Rect outRect = mTmpOutRect;
+ final int layoutDirection = getLayoutDirection();
+ final boolean isLayoutRtl = layoutDirection == LayoutDirection.RTL;
+ final boolean isPaddingNested = mLayerState.mPaddingMode == PADDING_MODE_NEST;
+ final ChildDrawable[] array = mLayerState.mChildren;
+
+ for (int i = 0, count = mLayerState.mNumChildren; i < count; i++) {
+ final ChildDrawable r = array[i];
+ final Drawable d = r.mDrawable;
+ if (d == null) {
+ continue;
+ }
+
+ final int insetT = r.mInsetT;
+ final int insetB = r.mInsetB;
+
+ // Resolve insets for RTL. Relative insets override absolute
+ // insets.
+ final int insetRtlL = isLayoutRtl ? r.mInsetE : r.mInsetS;
+ final int insetRtlR = isLayoutRtl ? r.mInsetS : r.mInsetE;
+ final int insetL = insetRtlL == INSET_UNDEFINED ? r.mInsetL : insetRtlL;
+ final int insetR = insetRtlR == INSET_UNDEFINED ? r.mInsetR : insetRtlR;
+
+ // Establish containing region based on aggregate padding and
+ // requested insets for the current layer.
+ final Rect container = mTmpContainer;
+ container.set(bounds.left + insetL + paddingL, bounds.top + insetT + paddingT,
+ bounds.right - insetR - paddingR, bounds.bottom - insetB - paddingB);
+
+ // Compute a reasonable default gravity based on the intrinsic and
+ // explicit dimensions, if specified.
+ final int intrinsicW = d.getIntrinsicWidth();
+ final int intrinsicH = d.getIntrinsicHeight();
+ final int layerW = r.mWidth;
+ final int layerH = r.mHeight;
+ final int gravity = resolveGravity(r.mGravity, layerW, layerH, intrinsicW, intrinsicH);
+
+ // Explicit dimensions override intrinsic dimensions.
+ final int resolvedW = layerW < 0 ? intrinsicW : layerW;
+ final int resolvedH = layerH < 0 ? intrinsicH : layerH;
+ Gravity.apply(gravity, resolvedW, resolvedH, container, outRect, layoutDirection);
+ d.setBounds(outRect);
+
+ if (isPaddingNested) {
+ paddingL += mPaddingL[i];
+ paddingR += mPaddingR[i];
+ paddingT += mPaddingT[i];
+ paddingB += mPaddingB[i];
+ }
+ }
+ }
+
+ /**
+ * Resolves layer gravity given explicit gravity and dimensions.
+ * <p>
+ * If the client hasn't specified a gravity but has specified an explicit
+ * dimension, defaults to START or TOP. Otherwise, defaults to FILL to
+ * preserve legacy behavior.
+ *
+ * @param gravity layer gravity
+ * @param width width of the layer if set, -1 otherwise
+ * @param height height of the layer if set, -1 otherwise
+ * @return the default gravity for the layer
+ */
+ private static int resolveGravity(int gravity, int width, int height,
+ int intrinsicWidth, int intrinsicHeight) {
+ if (!Gravity.isHorizontal(gravity)) {
+ if (width < 0) {
+ gravity |= Gravity.FILL_HORIZONTAL;
+ } else {
+ gravity |= Gravity.START;
+ }
+ }
+
+ if (!Gravity.isVertical(gravity)) {
+ if (height < 0) {
+ gravity |= Gravity.FILL_VERTICAL;
+ } else {
+ gravity |= Gravity.TOP;
+ }
+ }
+
+ // If a dimension if not specified, either implicitly or explicitly,
+ // force FILL for that dimension's gravity. This ensures that colors
+ // are handled correctly and ensures backward compatibility.
+ if (width < 0 && intrinsicWidth < 0) {
+ gravity |= Gravity.FILL_HORIZONTAL;
+ }
+
+ if (height < 0 && intrinsicHeight < 0) {
+ gravity |= Gravity.FILL_VERTICAL;
+ }
+
+ return gravity;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ int width = -1;
+ int padL = 0;
+ int padR = 0;
+
+ final boolean nest = mLayerState.mPaddingMode == PADDING_MODE_NEST;
+ final boolean isLayoutRtl = getLayoutDirection() == LayoutDirection.RTL;
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final ChildDrawable r = array[i];
+ if (r.mDrawable == null) {
+ continue;
+ }
+
+ // Take the resolved layout direction into account. If start / end
+ // padding are defined, they will be resolved (hence overriding) to
+ // left / right or right / left depending on the resolved layout
+ // direction. If start / end padding are not defined, use the
+ // left / right ones.
+ final int insetRtlL = isLayoutRtl ? r.mInsetE : r.mInsetS;
+ final int insetRtlR = isLayoutRtl ? r.mInsetS : r.mInsetE;
+ final int insetL = insetRtlL == INSET_UNDEFINED ? r.mInsetL : insetRtlL;
+ final int insetR = insetRtlR == INSET_UNDEFINED ? r.mInsetR : insetRtlR;
+
+ // Don't apply padding and insets for children that don't have
+ // an intrinsic dimension.
+ final int minWidth = r.mWidth < 0 ? r.mDrawable.getIntrinsicWidth() : r.mWidth;
+ final int w = minWidth < 0 ? -1 : minWidth + insetL + insetR + padL + padR;
+ if (w > width) {
+ width = w;
+ }
+
+ if (nest) {
+ padL += mPaddingL[i];
+ padR += mPaddingR[i];
+ }
+ }
+
+ return width;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ int height = -1;
+ int padT = 0;
+ int padB = 0;
+
+ final boolean nest = mLayerState.mPaddingMode == PADDING_MODE_NEST;
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final ChildDrawable r = array[i];
+ if (r.mDrawable == null) {
+ continue;
+ }
+
+ // Don't apply padding and insets for children that don't have
+ // an intrinsic dimension.
+ final int minHeight = r.mHeight < 0 ? r.mDrawable.getIntrinsicHeight() : r.mHeight;
+ final int h = minHeight < 0 ? -1 : minHeight + r.mInsetT + r.mInsetB + padT + padB;
+ if (h > height) {
+ height = h;
+ }
+
+ if (nest) {
+ padT += mPaddingT[i];
+ padB += mPaddingB[i];
+ }
+ }
+
+ return height;
+ }
+
+ /**
+ * Refreshes the cached padding values for the specified child.
+ *
+ * @return true if the child's padding has changed
+ */
+ private boolean refreshChildPadding(int i, ChildDrawable r) {
+ if (r.mDrawable != null) {
+ final Rect rect = mTmpRect;
+ r.mDrawable.getPadding(rect);
+ if (rect.left != mPaddingL[i] || rect.top != mPaddingT[i]
+ || rect.right != mPaddingR[i] || rect.bottom != mPaddingB[i]) {
+ mPaddingL[i] = rect.left;
+ mPaddingT[i] = rect.top;
+ mPaddingR[i] = rect.right;
+ mPaddingB[i] = rect.bottom;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Ensures the child padding caches are large enough.
+ */
+ void ensurePadding() {
+ final int N = mLayerState.mNumChildren;
+ if (mPaddingL != null && mPaddingL.length >= N) {
+ return;
+ }
+
+ mPaddingL = new int[N];
+ mPaddingT = new int[N];
+ mPaddingR = new int[N];
+ mPaddingB = new int[N];
+ }
+
+ void refreshPadding() {
+ final int N = mLayerState.mNumChildren;
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < N; i++) {
+ refreshChildPadding(i, array[i]);
+ }
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ if (mLayerState.canConstantState()) {
+ mLayerState.mChangingConfigurations = getChangingConfigurations();
+ return mLayerState;
+ }
+ return null;
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mLayerState = createConstantState(mLayerState, null);
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.mutate();
+ }
+ }
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ dr.clearMutated();
+ }
+ }
+ mMutated = false;
+ }
+
+ @Override
+ public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
+ boolean changed = false;
+
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int N = mLayerState.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ changed |= dr.setLayoutDirection(layoutDirection);
+ }
+ }
+
+ updateLayerBounds(getBounds());
+ return changed;
+ }
+
+ static class ChildDrawable {
+ public Drawable mDrawable;
+ public int[] mThemeAttrs;
+ public int mDensity = DisplayMetrics.DENSITY_DEFAULT;
+ public int mInsetL, mInsetT, mInsetR, mInsetB;
+ public int mInsetS = INSET_UNDEFINED;
+ public int mInsetE = INSET_UNDEFINED;
+ public int mWidth = -1;
+ public int mHeight = -1;
+ public int mGravity = Gravity.NO_GRAVITY;
+ public int mId = View.NO_ID;
+
+ ChildDrawable(int density) {
+ mDensity = density;
+ }
+
+ ChildDrawable(@NonNull ChildDrawable orig, @NonNull LayerDrawable owner,
+ @Nullable Resources res) {
+ final Drawable dr = orig.mDrawable;
+ final Drawable clone;
+ if (dr != null) {
+ final ConstantState cs = dr.getConstantState();
+ if (cs == null) {
+ clone = dr;
+ if (dr.getCallback() != null) {
+ // This drawable already has an owner.
+ Log.w(LOG_TAG, "Invalid drawable added to LayerDrawable! Drawable already "
+ + "belongs to another owner but does not expose a constant state.",
+ new RuntimeException());
+ }
+ } else if (res != null) {
+ clone = cs.newDrawable(res);
+ } else {
+ clone = cs.newDrawable();
+ }
+ clone.setLayoutDirection(dr.getLayoutDirection());
+ clone.setBounds(dr.getBounds());
+ clone.setLevel(dr.getLevel());
+
+ // Set the callback last to prevent invalidation from
+ // propagating before the constant state has been set.
+ clone.setCallback(owner);
+ } else {
+ clone = null;
+ }
+
+ mDrawable = clone;
+ mThemeAttrs = orig.mThemeAttrs;
+ mInsetL = orig.mInsetL;
+ mInsetT = orig.mInsetT;
+ mInsetR = orig.mInsetR;
+ mInsetB = orig.mInsetB;
+ mInsetS = orig.mInsetS;
+ mInsetE = orig.mInsetE;
+ mWidth = orig.mWidth;
+ mHeight = orig.mHeight;
+ mGravity = orig.mGravity;
+ mId = orig.mId;
+
+ mDensity = Drawable.resolveDensity(res, orig.mDensity);
+ if (orig.mDensity != mDensity) {
+ applyDensityScaling(orig.mDensity, mDensity);
+ }
+ }
+
+ public boolean canApplyTheme() {
+ return mThemeAttrs != null
+ || (mDrawable != null && mDrawable.canApplyTheme());
+ }
+
+ public final void setDensity(int targetDensity) {
+ if (mDensity != targetDensity) {
+ final int sourceDensity = mDensity;
+ mDensity = targetDensity;
+
+ applyDensityScaling(sourceDensity, targetDensity);
+ }
+ }
+
+ private void applyDensityScaling(int sourceDensity, int targetDensity) {
+ mInsetL = Drawable.scaleFromDensity(mInsetL, sourceDensity, targetDensity, false);
+ mInsetT = Drawable.scaleFromDensity(mInsetT, sourceDensity, targetDensity, false);
+ mInsetR = Drawable.scaleFromDensity(mInsetR, sourceDensity, targetDensity, false);
+ mInsetB = Drawable.scaleFromDensity(mInsetB, sourceDensity, targetDensity, false);
+ if (mInsetS != INSET_UNDEFINED) {
+ mInsetS = Drawable.scaleFromDensity(mInsetS, sourceDensity, targetDensity, false);
+ }
+ if (mInsetE != INSET_UNDEFINED) {
+ mInsetE = Drawable.scaleFromDensity(mInsetE, sourceDensity, targetDensity, false);
+ }
+ if (mWidth > 0) {
+ mWidth = Drawable.scaleFromDensity(mWidth, sourceDensity, targetDensity, true);
+ }
+ if (mHeight > 0) {
+ mHeight = Drawable.scaleFromDensity(mHeight, sourceDensity, targetDensity, true);
+ }
+ }
+ }
+
+ static class LayerState extends ConstantState {
+ private int[] mThemeAttrs;
+
+ int mNumChildren;
+ ChildDrawable[] mChildren;
+
+ int mDensity;
+
+ // These values all correspond to mDensity.
+ int mPaddingTop = -1;
+ int mPaddingBottom = -1;
+ int mPaddingLeft = -1;
+ int mPaddingRight = -1;
+ int mPaddingStart = -1;
+ int mPaddingEnd = -1;
+ int mOpacityOverride = PixelFormat.UNKNOWN;
+
+ @Config int mChangingConfigurations;
+ @Config int mChildrenChangingConfigurations;
+
+ private boolean mCheckedOpacity;
+ private int mOpacity;
+
+ private boolean mCheckedStateful;
+ private boolean mIsStateful;
+
+ private boolean mAutoMirrored = false;
+
+ private int mPaddingMode = PADDING_MODE_NEST;
+
+ LayerState(@Nullable LayerState orig, @NonNull LayerDrawable owner,
+ @Nullable Resources res) {
+ mDensity = Drawable.resolveDensity(res, orig != null ? orig.mDensity : 0);
+
+ if (orig != null) {
+ final ChildDrawable[] origChildDrawable = orig.mChildren;
+ final int N = orig.mNumChildren;
+
+ mNumChildren = N;
+ mChildren = new ChildDrawable[N];
+
+ mChangingConfigurations = orig.mChangingConfigurations;
+ mChildrenChangingConfigurations = orig.mChildrenChangingConfigurations;
+
+ for (int i = 0; i < N; i++) {
+ final ChildDrawable or = origChildDrawable[i];
+ mChildren[i] = new ChildDrawable(or, owner, res);
+ }
+
+ mCheckedOpacity = orig.mCheckedOpacity;
+ mOpacity = orig.mOpacity;
+ mCheckedStateful = orig.mCheckedStateful;
+ mIsStateful = orig.mIsStateful;
+ mAutoMirrored = orig.mAutoMirrored;
+ mPaddingMode = orig.mPaddingMode;
+ mThemeAttrs = orig.mThemeAttrs;
+ mPaddingTop = orig.mPaddingTop;
+ mPaddingBottom = orig.mPaddingBottom;
+ mPaddingLeft = orig.mPaddingLeft;
+ mPaddingRight = orig.mPaddingRight;
+ mPaddingStart = orig.mPaddingStart;
+ mPaddingEnd = orig.mPaddingEnd;
+ mOpacityOverride = orig.mOpacityOverride;
+
+ if (orig.mDensity != mDensity) {
+ applyDensityScaling(orig.mDensity, mDensity);
+ }
+ } else {
+ mNumChildren = 0;
+ mChildren = null;
+ }
+ }
+
+ public final void setDensity(int targetDensity) {
+ if (mDensity != targetDensity) {
+ final int sourceDensity = mDensity;
+ mDensity = targetDensity;
+
+ onDensityChanged(sourceDensity, targetDensity);
+ }
+ }
+
+ protected void onDensityChanged(int sourceDensity, int targetDensity) {
+ applyDensityScaling(sourceDensity, targetDensity);
+ }
+
+ private void applyDensityScaling(int sourceDensity, int targetDensity) {
+ if (mPaddingLeft > 0) {
+ mPaddingLeft = Drawable.scaleFromDensity(
+ mPaddingLeft, sourceDensity, targetDensity, false);
+ }
+ if (mPaddingTop > 0) {
+ mPaddingTop = Drawable.scaleFromDensity(
+ mPaddingTop, sourceDensity, targetDensity, false);
+ }
+ if (mPaddingRight > 0) {
+ mPaddingRight = Drawable.scaleFromDensity(
+ mPaddingRight, sourceDensity, targetDensity, false);
+ }
+ if (mPaddingBottom > 0) {
+ mPaddingBottom = Drawable.scaleFromDensity(
+ mPaddingBottom, sourceDensity, targetDensity, false);
+ }
+ if (mPaddingStart > 0) {
+ mPaddingStart = Drawable.scaleFromDensity(
+ mPaddingStart, sourceDensity, targetDensity, false);
+ }
+ if (mPaddingEnd > 0) {
+ mPaddingEnd = Drawable.scaleFromDensity(
+ mPaddingEnd, sourceDensity, targetDensity, false);
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ if (mThemeAttrs != null || super.canApplyTheme()) {
+ return true;
+ }
+
+ final ChildDrawable[] array = mChildren;
+ final int N = mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final ChildDrawable layer = array[i];
+ if (layer.canApplyTheme()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new LayerDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(@Nullable Resources res) {
+ return new LayerDrawable(this, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations
+ | mChildrenChangingConfigurations;
+ }
+
+ public final int getOpacity() {
+ if (mCheckedOpacity) {
+ return mOpacity;
+ }
+
+ final int N = mNumChildren;
+ final ChildDrawable[] array = mChildren;
+
+ // Seek to the first non-null drawable.
+ int firstIndex = -1;
+ for (int i = 0; i < N; i++) {
+ if (array[i].mDrawable != null) {
+ firstIndex = i;
+ break;
+ }
+ }
+
+ int op;
+ if (firstIndex >= 0) {
+ op = array[firstIndex].mDrawable.getOpacity();
+ } else {
+ op = PixelFormat.TRANSPARENT;
+ }
+
+ // Merge all remaining non-null drawables.
+ for (int i = firstIndex + 1; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null) {
+ op = Drawable.resolveOpacity(op, dr.getOpacity());
+ }
+ }
+
+ mOpacity = op;
+ mCheckedOpacity = true;
+ return op;
+ }
+
+ public final boolean isStateful() {
+ if (mCheckedStateful) {
+ return mIsStateful;
+ }
+
+ final int N = mNumChildren;
+ final ChildDrawable[] array = mChildren;
+ boolean isStateful = false;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null && dr.isStateful()) {
+ isStateful = true;
+ break;
+ }
+ }
+
+ mIsStateful = isStateful;
+ mCheckedStateful = true;
+ return isStateful;
+ }
+
+ public final boolean hasFocusStateSpecified() {
+ final int N = mNumChildren;
+ final ChildDrawable[] array = mChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null && dr.hasFocusStateSpecified()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public final boolean canConstantState() {
+ final ChildDrawable[] array = mChildren;
+ final int N = mNumChildren;
+ for (int i = 0; i < N; i++) {
+ final Drawable dr = array[i].mDrawable;
+ if (dr != null && dr.getConstantState() == null) {
+ return false;
+ }
+ }
+
+ // Don't cache the result, this method is not called very often.
+ return true;
+ }
+
+ /**
+ * Invalidates the cached opacity and statefulness.
+ */
+ void invalidateCache() {
+ mCheckedOpacity = false;
+ mCheckedStateful = false;
+ }
+
+ }
+}
+
diff --git a/android/graphics/drawable/LevelListDrawable.java b/android/graphics/drawable/LevelListDrawable.java
new file mode 100644
index 0000000..4ce52d1
--- /dev/null
+++ b/android/graphics/drawable/LevelListDrawable.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import java.io.IOException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.NonNull;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.Resources.Theme;
+import android.util.AttributeSet;
+
+/**
+ * A resource that manages a number of alternate Drawables, each assigned a maximum numerical value.
+ * Setting the level value of the object with {@link #setLevel(int)} will load the image with the next
+ * greater or equal value assigned to its max attribute.
+ * A good example use of
+ * a LevelListDrawable would be a battery level indicator icon, with different images to indicate the current
+ * battery level.
+ * <p>
+ * It can be defined in an XML file with the <code><level-list></code> element.
+ * Each Drawable level is defined in a nested <code><item></code>. For example:
+ * </p>
+ * <pre>
+ * <level-list xmlns:android="http://schemas.android.com/apk/res/android">
+ * <item android:maxLevel="0" android:drawable="@drawable/ic_wifi_signal_1" />
+ * <item android:maxLevel="1" android:drawable="@drawable/ic_wifi_signal_2" />
+ * <item android:maxLevel="2" android:drawable="@drawable/ic_wifi_signal_3" />
+ * <item android:maxLevel="3" android:drawable="@drawable/ic_wifi_signal_4" />
+ * </level-list>
+ *</pre>
+ * <p>With this XML saved into the res/drawable/ folder of the project, it can be referenced as
+ * the drawable for an {@link android.widget.ImageView}. The default image is the first in the list.
+ * It can then be changed to one of the other levels with
+ * {@link android.widget.ImageView#setImageLevel(int)}. For more
+ * information, see the guide to <a
+ * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
+ *
+ * @attr ref android.R.styleable#LevelListDrawableItem_minLevel
+ * @attr ref android.R.styleable#LevelListDrawableItem_maxLevel
+ * @attr ref android.R.styleable#LevelListDrawableItem_drawable
+ */
+public class LevelListDrawable extends DrawableContainer {
+ private LevelListState mLevelListState;
+ private boolean mMutated;
+
+ public LevelListDrawable() {
+ this(null, null);
+ }
+
+ public void addLevel(int low, int high, Drawable drawable) {
+ if (drawable != null) {
+ mLevelListState.addLevel(low, high, drawable);
+ // in case the new state matches our current state...
+ onLevelChange(getLevel());
+ }
+ }
+
+ // overrides from Drawable
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ int idx = mLevelListState.indexOfLevel(level);
+ if (selectDrawable(idx)) {
+ return true;
+ }
+ return super.onLevelChange(level);
+ }
+
+ @Override
+ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
+ throws XmlPullParserException, IOException {
+ super.inflate(r, parser, attrs, theme);
+ updateDensity(r);
+
+ inflateChildElements(r, parser, attrs, theme);
+ }
+
+ private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
+ Theme theme) throws XmlPullParserException, IOException {
+ int type;
+
+ int low = 0;
+
+ final int innerDepth = parser.getDepth() + 1;
+ int depth;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth
+ || type != XmlPullParser.END_TAG)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (depth > innerDepth || !parser.getName().equals("item")) {
+ continue;
+ }
+
+ TypedArray a = obtainAttributes(r, theme, attrs,
+ com.android.internal.R.styleable.LevelListDrawableItem);
+
+ low = a.getInt(
+ com.android.internal.R.styleable.LevelListDrawableItem_minLevel, 0);
+ int high = a.getInt(
+ com.android.internal.R.styleable.LevelListDrawableItem_maxLevel, 0);
+ int drawableRes = a.getResourceId(
+ com.android.internal.R.styleable.LevelListDrawableItem_drawable, 0);
+
+ a.recycle();
+
+ if (high < 0) {
+ throw new XmlPullParserException(parser.getPositionDescription()
+ + ": <item> tag requires a 'maxLevel' attribute");
+ }
+
+ Drawable dr;
+ if (drawableRes != 0) {
+ dr = r.getDrawable(drawableRes, theme);
+ } else {
+ while ((type = parser.next()) == XmlPullParser.TEXT) {
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException(
+ parser.getPositionDescription()
+ + ": <item> tag requires a 'drawable' attribute or "
+ + "child tag defining a drawable");
+ }
+ dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
+ }
+
+ mLevelListState.addLevel(low, high, dr);
+ }
+
+ onLevelChange(getLevel());
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mLevelListState.mutate();
+ mMutated = true;
+ }
+ return this;
+ }
+
+ @Override
+ LevelListState cloneConstantState() {
+ return new LevelListState(mLevelListState, this, null);
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mMutated = false;
+ }
+
+ private final static class LevelListState extends DrawableContainerState {
+ private int[] mLows;
+ private int[] mHighs;
+
+ LevelListState(LevelListState orig, LevelListDrawable owner, Resources res) {
+ super(orig, owner, res);
+
+ if (orig != null) {
+ // Perform a shallow copy and rely on mutate() to deep-copy.
+ mLows = orig.mLows;
+ mHighs = orig.mHighs;
+ } else {
+ mLows = new int[getCapacity()];
+ mHighs = new int[getCapacity()];
+ }
+ }
+
+ private void mutate() {
+ mLows = mLows.clone();
+ mHighs = mHighs.clone();
+ }
+
+ public void addLevel(int low, int high, Drawable drawable) {
+ int pos = addChild(drawable);
+ mLows[pos] = low;
+ mHighs[pos] = high;
+ }
+
+ public int indexOfLevel(int level) {
+ final int[] lows = mLows;
+ final int[] highs = mHighs;
+ final int N = getChildCount();
+ for (int i = 0; i < N; i++) {
+ if (level >= lows[i] && level <= highs[i]) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new LevelListDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new LevelListDrawable(this, res);
+ }
+
+ @Override
+ public void growArray(int oldSize, int newSize) {
+ super.growArray(oldSize, newSize);
+ int[] newInts = new int[newSize];
+ System.arraycopy(mLows, 0, newInts, 0, oldSize);
+ mLows = newInts;
+ newInts = new int[newSize];
+ System.arraycopy(mHighs, 0, newInts, 0, oldSize);
+ mHighs = newInts;
+ }
+ }
+
+ @Override
+ protected void setConstantState(@NonNull DrawableContainerState state) {
+ super.setConstantState(state);
+
+ if (state instanceof LevelListState) {
+ mLevelListState = (LevelListState) state;
+ }
+ }
+
+ private LevelListDrawable(LevelListState state, Resources res) {
+ final LevelListState as = new LevelListState(state, this, res);
+ setConstantState(as);
+ onLevelChange(getLevel());
+ }
+}
+
diff --git a/android/graphics/drawable/NinePatchDrawable.java b/android/graphics/drawable/NinePatchDrawable.java
new file mode 100644
index 0000000..1790020
--- /dev/null
+++ b/android/graphics/drawable/NinePatchDrawable.java
@@ -0,0 +1,749 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Insets;
+import android.graphics.NinePatch;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.LayoutDirection;
+import android.util.TypedValue;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ *
+ * A resizeable bitmap, with stretchable areas that you define. This type of image
+ * is defined in a .png file with a special format.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about how to use a NinePatchDrawable, read the
+ * <a href="{@docRoot}guide/topics/graphics/2d-graphics.html#nine-patch">
+ * Canvas and Drawables</a> developer guide. For information about creating a NinePatch image
+ * file using the draw9patch tool, see the
+ * <a href="{@docRoot}guide/developing/tools/draw9patch.html">Draw 9-patch</a> tool guide.</p></div>
+ */
+public class NinePatchDrawable extends Drawable {
+ // dithering helps a lot, and is pretty cheap, so default is true
+ private static final boolean DEFAULT_DITHER = false;
+
+ /** Temporary rect used for density scaling. */
+ private Rect mTempRect;
+
+ private NinePatchState mNinePatchState;
+ private PorterDuffColorFilter mTintFilter;
+ private Rect mPadding;
+ private Insets mOpticalInsets = Insets.NONE;
+ private Rect mOutlineInsets;
+ private float mOutlineRadius;
+ private Paint mPaint;
+ private boolean mMutated;
+
+ private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
+
+ // These are scaled to match the target density.
+ private int mBitmapWidth = -1;
+ private int mBitmapHeight = -1;
+
+ NinePatchDrawable() {
+ mNinePatchState = new NinePatchState();
+ }
+
+ /**
+ * Create drawable from raw nine-patch data, not dealing with density.
+ *
+ * @deprecated Use {@link #NinePatchDrawable(Resources, Bitmap, byte[], Rect, String)}
+ * to ensure that the drawable has correctly set its target density.
+ */
+ @Deprecated
+ public NinePatchDrawable(Bitmap bitmap, byte[] chunk, Rect padding, String srcName) {
+ this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding), null);
+ }
+
+ /**
+ * Create drawable from raw nine-patch data, setting initial target density
+ * based on the display metrics of the resources.
+ */
+ public NinePatchDrawable(Resources res, Bitmap bitmap, byte[] chunk,
+ Rect padding, String srcName) {
+ this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding), res);
+ }
+
+ /**
+ * Create drawable from raw nine-patch data, setting initial target density
+ * based on the display metrics of the resources.
+ *
+ * @hide
+ */
+ public NinePatchDrawable(Resources res, Bitmap bitmap, byte[] chunk,
+ Rect padding, Rect opticalInsets, String srcName) {
+ this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding, opticalInsets),
+ res);
+ }
+
+ /**
+ * Create drawable from existing nine-patch, not dealing with density.
+ *
+ * @deprecated Use {@link #NinePatchDrawable(Resources, NinePatch)}
+ * to ensure that the drawable has correctly set its target
+ * density.
+ */
+ @Deprecated
+ public NinePatchDrawable(@NonNull NinePatch patch) {
+ this(new NinePatchState(patch, new Rect()), null);
+ }
+
+ /**
+ * Create drawable from existing nine-patch, setting initial target density
+ * based on the display metrics of the resources.
+ */
+ public NinePatchDrawable(@Nullable Resources res, @NonNull NinePatch patch) {
+ this(new NinePatchState(patch, new Rect()), res);
+ }
+
+ /**
+ * Set the density scale at which this drawable will be rendered. This
+ * method assumes the drawable will be rendered at the same density as the
+ * specified canvas.
+ *
+ * @param canvas The Canvas from which the density scale must be obtained.
+ *
+ * @see android.graphics.Bitmap#setDensity(int)
+ * @see android.graphics.Bitmap#getDensity()
+ */
+ public void setTargetDensity(@NonNull Canvas canvas) {
+ setTargetDensity(canvas.getDensity());
+ }
+
+ /**
+ * Set the density scale at which this drawable will be rendered.
+ *
+ * @param metrics The DisplayMetrics indicating the density scale for this drawable.
+ *
+ * @see android.graphics.Bitmap#setDensity(int)
+ * @see android.graphics.Bitmap#getDensity()
+ */
+ public void setTargetDensity(@NonNull DisplayMetrics metrics) {
+ setTargetDensity(metrics.densityDpi);
+ }
+
+ /**
+ * Set the density at which this drawable will be rendered.
+ *
+ * @param density The density scale for this drawable.
+ *
+ * @see android.graphics.Bitmap#setDensity(int)
+ * @see android.graphics.Bitmap#getDensity()
+ */
+ public void setTargetDensity(int density) {
+ if (density == 0) {
+ density = DisplayMetrics.DENSITY_DEFAULT;
+ }
+
+ if (mTargetDensity != density) {
+ mTargetDensity = density;
+
+ computeBitmapSize();
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final NinePatchState state = mNinePatchState;
+
+ Rect bounds = getBounds();
+ int restoreToCount = -1;
+
+ final boolean clearColorFilter;
+ if (mTintFilter != null && getPaint().getColorFilter() == null) {
+ mPaint.setColorFilter(mTintFilter);
+ clearColorFilter = true;
+ } else {
+ clearColorFilter = false;
+ }
+
+ final int restoreAlpha;
+ if (state.mBaseAlpha != 1.0f) {
+ restoreAlpha = getPaint().getAlpha();
+ mPaint.setAlpha((int) (restoreAlpha * state.mBaseAlpha + 0.5f));
+ } else {
+ restoreAlpha = -1;
+ }
+
+ final boolean needsDensityScaling = canvas.getDensity() == 0;
+ if (needsDensityScaling) {
+ restoreToCount = restoreToCount >= 0 ? restoreToCount : canvas.save();
+
+ // Apply density scaling.
+ final float scale = mTargetDensity / (float) state.mNinePatch.getDensity();
+ final float px = bounds.left;
+ final float py = bounds.top;
+ canvas.scale(scale, scale, px, py);
+
+ if (mTempRect == null) {
+ mTempRect = new Rect();
+ }
+
+ // Scale the bounds to match.
+ final Rect scaledBounds = mTempRect;
+ scaledBounds.left = bounds.left;
+ scaledBounds.top = bounds.top;
+ scaledBounds.right = bounds.left + Math.round(bounds.width() / scale);
+ scaledBounds.bottom = bounds.top + Math.round(bounds.height() / scale);
+ bounds = scaledBounds;
+ }
+
+ final boolean needsMirroring = needsMirroring();
+ if (needsMirroring) {
+ restoreToCount = restoreToCount >= 0 ? restoreToCount : canvas.save();
+
+ // Mirror the 9patch.
+ final float cx = (bounds.left + bounds.right) / 2.0f;
+ final float cy = (bounds.top + bounds.bottom) / 2.0f;
+ canvas.scale(-1.0f, 1.0f, cx, cy);
+ }
+
+ state.mNinePatch.draw(canvas, bounds, mPaint);
+
+ if (restoreToCount >= 0) {
+ canvas.restoreToCount(restoreToCount);
+ }
+
+ if (clearColorFilter) {
+ mPaint.setColorFilter(null);
+ }
+
+ if (restoreAlpha >= 0) {
+ mPaint.setAlpha(restoreAlpha);
+ }
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations() | mNinePatchState.getChangingConfigurations();
+ }
+
+ @Override
+ public boolean getPadding(@NonNull Rect padding) {
+ if (mPadding != null) {
+ padding.set(mPadding);
+ return (padding.left | padding.top | padding.right | padding.bottom) != 0;
+ } else {
+ return super.getPadding(padding);
+ }
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ final Rect bounds = getBounds();
+ if (bounds.isEmpty()) {
+ return;
+ }
+
+ if (mNinePatchState != null && mOutlineInsets != null) {
+ final NinePatch.InsetStruct insets =
+ mNinePatchState.mNinePatch.getBitmap().getNinePatchInsets();
+ if (insets != null) {
+ outline.setRoundRect(bounds.left + mOutlineInsets.left,
+ bounds.top + mOutlineInsets.top,
+ bounds.right - mOutlineInsets.right,
+ bounds.bottom - mOutlineInsets.bottom,
+ mOutlineRadius);
+ outline.setAlpha(insets.outlineAlpha * (getAlpha() / 255.0f));
+ return;
+ }
+ }
+
+ super.getOutline(outline);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public Insets getOpticalInsets() {
+ final Insets opticalInsets = mOpticalInsets;
+ if (needsMirroring()) {
+ return Insets.of(opticalInsets.right, opticalInsets.top,
+ opticalInsets.left, opticalInsets.bottom);
+ } else {
+ return opticalInsets;
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (mPaint == null && alpha == 0xFF) {
+ // Fast common case -- leave at normal alpha.
+ return;
+ }
+ getPaint().setAlpha(alpha);
+ invalidateSelf();
+ }
+
+ @Override
+ public int getAlpha() {
+ if (mPaint == null) {
+ // Fast common case -- normal alpha.
+ return 0xFF;
+ }
+ return getPaint().getAlpha();
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ if (mPaint == null && colorFilter == null) {
+ // Fast common case -- leave at no color filter.
+ return;
+ }
+ getPaint().setColorFilter(colorFilter);
+ invalidateSelf();
+ }
+
+ @Override
+ public void setTintList(@Nullable ColorStateList tint) {
+ mNinePatchState.mTint = tint;
+ mTintFilter = updateTintFilter(mTintFilter, tint, mNinePatchState.mTintMode);
+ invalidateSelf();
+ }
+
+ @Override
+ public void setTintMode(@Nullable PorterDuff.Mode tintMode) {
+ mNinePatchState.mTintMode = tintMode;
+ mTintFilter = updateTintFilter(mTintFilter, mNinePatchState.mTint, tintMode);
+ invalidateSelf();
+ }
+
+ @Override
+ public void setDither(boolean dither) {
+ //noinspection PointlessBooleanExpression
+ if (mPaint == null && dither == DEFAULT_DITHER) {
+ // Fast common case -- leave at default dither.
+ return;
+ }
+
+ getPaint().setDither(dither);
+ invalidateSelf();
+ }
+
+ @Override
+ public void setAutoMirrored(boolean mirrored) {
+ mNinePatchState.mAutoMirrored = mirrored;
+ }
+
+ private boolean needsMirroring() {
+ return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL;
+ }
+
+ @Override
+ public boolean isAutoMirrored() {
+ return mNinePatchState.mAutoMirrored;
+ }
+
+ @Override
+ public void setFilterBitmap(boolean filter) {
+ getPaint().setFilterBitmap(filter);
+ invalidateSelf();
+ }
+
+ @Override
+ public boolean isFilterBitmap() {
+ return mPaint != null && getPaint().isFilterBitmap();
+ }
+
+ @Override
+ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
+ throws XmlPullParserException, IOException {
+ super.inflate(r, parser, attrs, theme);
+
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.NinePatchDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+
+ updateLocalState(r);
+ }
+
+ /**
+ * Updates the constant state from the values in the typed array.
+ */
+ private void updateStateFromTypedArray(@NonNull TypedArray a) throws XmlPullParserException {
+ final Resources r = a.getResources();
+ final NinePatchState state = mNinePatchState;
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ state.mDither = a.getBoolean(R.styleable.NinePatchDrawable_dither, state.mDither);
+
+ final int srcResId = a.getResourceId(R.styleable.NinePatchDrawable_src, 0);
+ if (srcResId != 0) {
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inDither = !state.mDither;
+ options.inScreenDensity = r.getDisplayMetrics().noncompatDensityDpi;
+
+ final Rect padding = new Rect();
+ final Rect opticalInsets = new Rect();
+ Bitmap bitmap = null;
+
+ try {
+ final TypedValue value = new TypedValue();
+ final InputStream is = r.openRawResource(srcResId, value);
+
+ bitmap = BitmapFactory.decodeResourceStream(r, value, is, padding, options);
+
+ is.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+
+ if (bitmap == null) {
+ throw new XmlPullParserException(a.getPositionDescription() +
+ ": <nine-patch> requires a valid src attribute");
+ } else if (bitmap.getNinePatchChunk() == null) {
+ throw new XmlPullParserException(a.getPositionDescription() +
+ ": <nine-patch> requires a valid 9-patch source image");
+ }
+
+ bitmap.getOpticalInsets(opticalInsets);
+
+ state.mNinePatch = new NinePatch(bitmap, bitmap.getNinePatchChunk());
+ state.mPadding = padding;
+ state.mOpticalInsets = Insets.of(opticalInsets);
+ }
+
+ state.mAutoMirrored = a.getBoolean(
+ R.styleable.NinePatchDrawable_autoMirrored, state.mAutoMirrored);
+ state.mBaseAlpha = a.getFloat(R.styleable.NinePatchDrawable_alpha, state.mBaseAlpha);
+
+ final int tintMode = a.getInt(R.styleable.NinePatchDrawable_tintMode, -1);
+ if (tintMode != -1) {
+ state.mTintMode = Drawable.parseTintMode(tintMode, Mode.SRC_IN);
+ }
+
+ final ColorStateList tint = a.getColorStateList(R.styleable.NinePatchDrawable_tint);
+ if (tint != null) {
+ state.mTint = tint;
+ }
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ final NinePatchState state = mNinePatchState;
+ if (state == null) {
+ return;
+ }
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(
+ state.mThemeAttrs, R.styleable.NinePatchDrawable);
+ try {
+ updateStateFromTypedArray(a);
+ } catch (XmlPullParserException e) {
+ rethrowAsRuntimeException(e);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ if (state.mTint != null && state.mTint.canApplyTheme()) {
+ state.mTint = state.mTint.obtainForTheme(t);
+ }
+
+ updateLocalState(t.getResources());
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mNinePatchState != null && mNinePatchState.canApplyTheme();
+ }
+
+ @NonNull
+ public Paint getPaint() {
+ if (mPaint == null) {
+ mPaint = new Paint();
+ mPaint.setDither(DEFAULT_DITHER);
+ }
+ return mPaint;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mBitmapWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mBitmapHeight;
+ }
+
+ @Override
+ public int getOpacity() {
+ return mNinePatchState.mNinePatch.hasAlpha()
+ || (mPaint != null && mPaint.getAlpha() < 255) ?
+ PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
+ }
+
+ @Override
+ public Region getTransparentRegion() {
+ return mNinePatchState.mNinePatch.getTransparentRegion(getBounds());
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ mNinePatchState.mChangingConfigurations = getChangingConfigurations();
+ return mNinePatchState;
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mNinePatchState = new NinePatchState(mNinePatchState);
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mMutated = false;
+ }
+
+ @Override
+ protected boolean onStateChange(int[] stateSet) {
+ final NinePatchState state = mNinePatchState;
+ if (state.mTint != null && state.mTintMode != null) {
+ mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean isStateful() {
+ final NinePatchState s = mNinePatchState;
+ return super.isStateful() || (s.mTint != null && s.mTint.isStateful());
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return mNinePatchState.mTint != null && mNinePatchState.mTint.hasFocusStateSpecified();
+ }
+
+ final static class NinePatchState extends ConstantState {
+ @Config int mChangingConfigurations;
+
+ // Values loaded during inflation.
+ NinePatch mNinePatch = null;
+ ColorStateList mTint = null;
+ Mode mTintMode = DEFAULT_TINT_MODE;
+ Rect mPadding = null;
+ Insets mOpticalInsets = Insets.NONE;
+ float mBaseAlpha = 1.0f;
+ boolean mDither = DEFAULT_DITHER;
+ boolean mAutoMirrored = false;
+
+ int[] mThemeAttrs;
+
+ NinePatchState() {
+ // Empty constructor.
+ }
+
+ NinePatchState(@NonNull NinePatch ninePatch, @Nullable Rect padding) {
+ this(ninePatch, padding, null, DEFAULT_DITHER, false);
+ }
+
+ NinePatchState(@NonNull NinePatch ninePatch, @Nullable Rect padding,
+ @Nullable Rect opticalInsets) {
+ this(ninePatch, padding, opticalInsets, DEFAULT_DITHER, false);
+ }
+
+ NinePatchState(@NonNull NinePatch ninePatch, @Nullable Rect padding,
+ @Nullable Rect opticalInsets, boolean dither, boolean autoMirror) {
+ mNinePatch = ninePatch;
+ mPadding = padding;
+ mOpticalInsets = Insets.of(opticalInsets);
+ mDither = dither;
+ mAutoMirrored = autoMirror;
+ }
+
+ NinePatchState(@NonNull NinePatchState orig) {
+ mChangingConfigurations = orig.mChangingConfigurations;
+ mNinePatch = orig.mNinePatch;
+ mTint = orig.mTint;
+ mTintMode = orig.mTintMode;
+ mPadding = orig.mPadding;
+ mOpticalInsets = orig.mOpticalInsets;
+ mBaseAlpha = orig.mBaseAlpha;
+ mDither = orig.mDither;
+ mAutoMirrored = orig.mAutoMirrored;
+ mThemeAttrs = orig.mThemeAttrs;
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mThemeAttrs != null
+ || (mTint != null && mTint.canApplyTheme())
+ || super.canApplyTheme();
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new NinePatchDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new NinePatchDrawable(this, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations
+ | (mTint != null ? mTint.getChangingConfigurations() : 0);
+ }
+ }
+
+ private void computeBitmapSize() {
+ final NinePatch ninePatch = mNinePatchState.mNinePatch;
+ if (ninePatch == null) {
+ return;
+ }
+
+ final int sourceDensity = ninePatch.getDensity();
+ final int targetDensity = mTargetDensity;
+
+ final Insets sourceOpticalInsets = mNinePatchState.mOpticalInsets;
+ if (sourceOpticalInsets != Insets.NONE) {
+ final int left = Drawable.scaleFromDensity(
+ sourceOpticalInsets.left, sourceDensity, targetDensity, true);
+ final int top = Drawable.scaleFromDensity(
+ sourceOpticalInsets.top, sourceDensity, targetDensity, true);
+ final int right = Drawable.scaleFromDensity(
+ sourceOpticalInsets.right, sourceDensity, targetDensity, true);
+ final int bottom = Drawable.scaleFromDensity(
+ sourceOpticalInsets.bottom, sourceDensity, targetDensity, true);
+ mOpticalInsets = Insets.of(left, top, right, bottom);
+ } else {
+ mOpticalInsets = Insets.NONE;
+ }
+
+ final Rect sourcePadding = mNinePatchState.mPadding;
+ if (sourcePadding != null) {
+ if (mPadding == null) {
+ mPadding = new Rect();
+ }
+ mPadding.left = Drawable.scaleFromDensity(
+ sourcePadding.left, sourceDensity, targetDensity, false);
+ mPadding.top = Drawable.scaleFromDensity(
+ sourcePadding.top, sourceDensity, targetDensity, false);
+ mPadding.right = Drawable.scaleFromDensity(
+ sourcePadding.right, sourceDensity, targetDensity, false);
+ mPadding.bottom = Drawable.scaleFromDensity(
+ sourcePadding.bottom, sourceDensity, targetDensity, false);
+ } else {
+ mPadding = null;
+ }
+
+ mBitmapHeight = Drawable.scaleFromDensity(
+ ninePatch.getHeight(), sourceDensity, targetDensity, true);
+ mBitmapWidth = Drawable.scaleFromDensity(
+ ninePatch.getWidth(), sourceDensity, targetDensity, true);
+
+ final NinePatch.InsetStruct insets = ninePatch.getBitmap().getNinePatchInsets();
+ if (insets != null) {
+ Rect outlineRect = insets.outlineRect;
+ mOutlineInsets = NinePatch.InsetStruct.scaleInsets(outlineRect.left, outlineRect.top,
+ outlineRect.right, outlineRect.bottom, targetDensity / (float) sourceDensity);
+ mOutlineRadius = Drawable.scaleFromDensity(
+ insets.outlineRadius, sourceDensity, targetDensity);
+ } else {
+ mOutlineInsets = null;
+ }
+ }
+
+ /**
+ * The one constructor to rule them all. This is called by all public
+ * constructors to set the state and initialize local properties.
+ *
+ * @param state constant state to assign to the new drawable
+ */
+ private NinePatchDrawable(@NonNull NinePatchState state, @Nullable Resources res) {
+ mNinePatchState = state;
+
+ updateLocalState(res);
+ }
+
+ /**
+ * Initializes local dynamic properties from state.
+ */
+ private void updateLocalState(@Nullable Resources res) {
+ final NinePatchState state = mNinePatchState;
+
+ // If we can, avoid calling any methods that initialize Paint.
+ if (state.mDither != DEFAULT_DITHER) {
+ setDither(state.mDither);
+ }
+
+ // The nine-patch may have been created without a Resources object, in
+ // which case we should try to match the density of the nine patch (if
+ // available).
+ if (res == null && state.mNinePatch != null) {
+ mTargetDensity = state.mNinePatch.getDensity();
+ } else {
+ mTargetDensity = Drawable.resolveDensity(res, mTargetDensity);
+ }
+ mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
+ computeBitmapSize();
+ }
+}
diff --git a/android/graphics/drawable/PaintDrawable.java b/android/graphics/drawable/PaintDrawable.java
new file mode 100644
index 0000000..a82e7b9
--- /dev/null
+++ b/android/graphics/drawable/PaintDrawable.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Drawable that draws its bounds in the given paint, with optional
+ * rounded corners.
+*/
+public class PaintDrawable extends ShapeDrawable {
+
+ public PaintDrawable() {
+ }
+
+ public PaintDrawable(int color) {
+ getPaint().setColor(color);
+ }
+
+ /**
+ * Specify radius for the corners of the rectangle. If this is > 0, then the
+ * drawable is drawn in a round-rectangle, rather than a rectangle.
+ * @param radius the radius for the corners of the rectangle
+ */
+ public void setCornerRadius(float radius) {
+ float[] radii = null;
+ if (radius > 0) {
+ radii = new float[8];
+ for (int i = 0; i < 8; i++) {
+ radii[i] = radius;
+ }
+ }
+ setCornerRadii(radii);
+ }
+
+ /**
+ * Specify radii for each of the 4 corners. For each corner, the array
+ * contains 2 values, [X_radius, Y_radius]. The corners are ordered
+ * top-left, top-right, bottom-right, bottom-left
+ * @param radii the x and y radii of the corners
+ */
+ public void setCornerRadii(float[] radii) {
+ if (radii == null) {
+ if (getShape() != null) {
+ setShape(null);
+ }
+ } else {
+ setShape(new RoundRectShape(radii, null, null));
+ }
+ invalidateSelf();
+ }
+
+ @Override
+ protected boolean inflateTag(String name, Resources r, XmlPullParser parser,
+ AttributeSet attrs) {
+ if (name.equals("corners")) {
+ TypedArray a = r.obtainAttributes(attrs,
+ com.android.internal.R.styleable.DrawableCorners);
+ int radius = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.DrawableCorners_radius, 0);
+ setCornerRadius(radius);
+
+ // now check of they have any per-corner radii
+
+ int topLeftRadius = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.DrawableCorners_topLeftRadius, radius);
+ int topRightRadius = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.DrawableCorners_topRightRadius, radius);
+ int bottomLeftRadius = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.DrawableCorners_bottomLeftRadius, radius);
+ int bottomRightRadius = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.DrawableCorners_bottomRightRadius, radius);
+
+ if (topLeftRadius != radius || topRightRadius != radius ||
+ bottomLeftRadius != radius || bottomRightRadius != radius) {
+ setCornerRadii(new float[] {
+ topLeftRadius, topLeftRadius,
+ topRightRadius, topRightRadius,
+ bottomLeftRadius, bottomLeftRadius,
+ bottomRightRadius, bottomRightRadius
+ });
+ }
+ a.recycle();
+ return true;
+ }
+ return super.inflateTag(name, r, parser, attrs);
+ }
+}
+
diff --git a/android/graphics/drawable/PictureDrawable.java b/android/graphics/drawable/PictureDrawable.java
new file mode 100644
index 0000000..583cffb
--- /dev/null
+++ b/android/graphics/drawable/PictureDrawable.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2008 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 android.graphics.drawable;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.drawable.Drawable;
+import android.graphics.Picture;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+
+/**
+ * Drawable subclass that wraps a Picture, allowing the picture to be used
+ * wherever a Drawable is supported.
+ */
+public class PictureDrawable extends Drawable {
+
+ private Picture mPicture;
+
+ /**
+ * Construct a new drawable referencing the specified picture. The picture
+ * may be null.
+ *
+ * @param picture The picture to associate with the drawable. May be null.
+ */
+ public PictureDrawable(Picture picture) {
+ mPicture = picture;
+ }
+
+ /**
+ * Return the picture associated with the drawable. May be null.
+ *
+ * @return the picture associated with the drawable, or null.
+ */
+ public Picture getPicture() {
+ return mPicture;
+ }
+
+ /**
+ * Associate a picture with this drawable. The picture may be null.
+ *
+ * @param picture The picture to associate with the drawable. May be null.
+ */
+ public void setPicture(Picture picture) {
+ mPicture = picture;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mPicture != null) {
+ Rect bounds = getBounds();
+ canvas.save();
+ canvas.clipRect(bounds);
+ canvas.translate(bounds.left, bounds.top);
+ canvas.drawPicture(mPicture);
+ canvas.restore();
+ }
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mPicture != null ? mPicture.getWidth() : -1;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mPicture != null ? mPicture.getHeight() : -1;
+ }
+
+ @Override
+ public int getOpacity() {
+ // not sure, so be safe
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {}
+
+ @Override
+ public void setAlpha(int alpha) {}
+}
+
diff --git a/android/graphics/drawable/RippleBackground.java b/android/graphics/drawable/RippleBackground.java
new file mode 100644
index 0000000..6bd2646
--- /dev/null
+++ b/android/graphics/drawable/RippleBackground.java
@@ -0,0 +1,191 @@
+/*
+ * 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 android.graphics.drawable;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.FloatProperty;
+import android.view.DisplayListCanvas;
+import android.view.RenderNodeAnimator;
+import android.view.animation.LinearInterpolator;
+
+/**
+ * Draws a ripple background.
+ */
+class RippleBackground extends RippleComponent {
+
+ private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+
+ private static final int OPACITY_ENTER_DURATION = 600;
+ private static final int OPACITY_ENTER_DURATION_FAST = 120;
+ private static final int OPACITY_EXIT_DURATION = 480;
+
+ // Hardware rendering properties.
+ private CanvasProperty<Paint> mPropPaint;
+ private CanvasProperty<Float> mPropRadius;
+ private CanvasProperty<Float> mPropX;
+ private CanvasProperty<Float> mPropY;
+
+ // Software rendering properties.
+ private float mOpacity = 0;
+
+ /** Whether this ripple is bounded. */
+ private boolean mIsBounded;
+
+ public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded,
+ boolean forceSoftware) {
+ super(owner, bounds, forceSoftware);
+
+ mIsBounded = isBounded;
+ }
+
+ public boolean isVisible() {
+ return mOpacity > 0 || isHardwareAnimating();
+ }
+
+ @Override
+ protected boolean drawSoftware(Canvas c, Paint p) {
+ boolean hasContent = false;
+
+ final int origAlpha = p.getAlpha();
+ final int alpha = (int) (origAlpha * mOpacity + 0.5f);
+ if (alpha > 0) {
+ p.setAlpha(alpha);
+ c.drawCircle(0, 0, mTargetRadius, p);
+ p.setAlpha(origAlpha);
+ hasContent = true;
+ }
+
+ return hasContent;
+ }
+
+ @Override
+ protected boolean drawHardware(DisplayListCanvas c) {
+ c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
+ return true;
+ }
+
+ @Override
+ protected Animator createSoftwareEnter(boolean fast) {
+ // Linear enter based on current opacity.
+ final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION;
+ final int duration = (int) ((1 - mOpacity) * maxDuration);
+
+ final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
+ opacity.setAutoCancel(true);
+ opacity.setDuration(duration);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
+
+ return opacity;
+ }
+
+ @Override
+ protected Animator createSoftwareExit() {
+ final AnimatorSet set = new AnimatorSet();
+
+ // Linear exit after enter is completed.
+ final ObjectAnimator exit = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 0);
+ exit.setInterpolator(LINEAR_INTERPOLATOR);
+ exit.setDuration(OPACITY_EXIT_DURATION);
+ exit.setAutoCancel(true);
+
+ final AnimatorSet.Builder builder = set.play(exit);
+
+ // Linear "fast" enter based on current opacity.
+ final int fastEnterDuration = mIsBounded ?
+ (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0;
+ if (fastEnterDuration > 0) {
+ final ObjectAnimator enter = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 1);
+ enter.setInterpolator(LINEAR_INTERPOLATOR);
+ enter.setDuration(fastEnterDuration);
+ enter.setAutoCancel(true);
+
+ builder.after(enter);
+ }
+
+ return set;
+ }
+
+ @Override
+ protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
+ final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
+
+ final int targetAlpha = p.getAlpha();
+ final int currentAlpha = (int) (mOpacity * targetAlpha + 0.5f);
+ p.setAlpha(currentAlpha);
+
+ mPropPaint = CanvasProperty.createPaint(p);
+ mPropRadius = CanvasProperty.createFloat(mTargetRadius);
+ mPropX = CanvasProperty.createFloat(0);
+ mPropY = CanvasProperty.createFloat(0);
+
+ final int fastEnterDuration = mIsBounded ?
+ (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0;
+
+ // Linear exit after enter is completed.
+ final RenderNodeAnimator exit = new RenderNodeAnimator(
+ mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
+ exit.setInterpolator(LINEAR_INTERPOLATOR);
+ exit.setDuration(OPACITY_EXIT_DURATION);
+ if (fastEnterDuration > 0) {
+ exit.setStartDelay(fastEnterDuration);
+ exit.setStartValue(targetAlpha);
+ }
+ set.add(exit);
+
+ // Linear "fast" enter based on current opacity.
+ if (fastEnterDuration > 0) {
+ final RenderNodeAnimator enter = new RenderNodeAnimator(
+ mPropPaint, RenderNodeAnimator.PAINT_ALPHA, targetAlpha);
+ enter.setInterpolator(LINEAR_INTERPOLATOR);
+ enter.setDuration(fastEnterDuration);
+ set.add(enter);
+ }
+
+ return set;
+ }
+
+ @Override
+ protected void jumpValuesToExit() {
+ mOpacity = 0;
+ }
+
+ private static abstract class BackgroundProperty extends FloatProperty<RippleBackground> {
+ public BackgroundProperty(String name) {
+ super(name);
+ }
+ }
+
+ private static final BackgroundProperty OPACITY = new BackgroundProperty("opacity") {
+ @Override
+ public void setValue(RippleBackground object, float value) {
+ object.mOpacity = value;
+ object.invalidateSelf();
+ }
+
+ @Override
+ public Float get(RippleBackground object) {
+ return object.mOpacity;
+ }
+ };
+}
diff --git a/android/graphics/drawable/RippleComponent.java b/android/graphics/drawable/RippleComponent.java
new file mode 100644
index 0000000..e83513c
--- /dev/null
+++ b/android/graphics/drawable/RippleComponent.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2015 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 android.graphics.drawable;
+
+import android.animation.Animator;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.DisplayListCanvas;
+import android.view.RenderNodeAnimator;
+
+import java.util.ArrayList;
+
+/**
+ * Abstract class that handles hardware/software hand-off and lifecycle for
+ * animated ripple foreground and background components.
+ */
+abstract class RippleComponent {
+ private final RippleDrawable mOwner;
+
+ /** Bounds used for computing max radius. May be modified by the owner. */
+ protected final Rect mBounds;
+
+ /** Whether we can use hardware acceleration for the exit animation. */
+ private boolean mHasDisplayListCanvas;
+
+ private boolean mHasPendingHardwareAnimator;
+ private RenderNodeAnimatorSet mHardwareAnimator;
+
+ private Animator mSoftwareAnimator;
+
+ /** Whether we have an explicit maximum radius. */
+ private boolean mHasMaxRadius;
+
+ /** How big this ripple should be when fully entered. */
+ protected float mTargetRadius;
+
+ /** Screen density used to adjust pixel-based constants. */
+ protected float mDensityScale;
+
+ /**
+ * If set, force all ripple animations to not run on RenderThread, even if it would be
+ * available.
+ */
+ private final boolean mForceSoftware;
+
+ public RippleComponent(RippleDrawable owner, Rect bounds, boolean forceSoftware) {
+ mOwner = owner;
+ mBounds = bounds;
+ mForceSoftware = forceSoftware;
+ }
+
+ public void onBoundsChange() {
+ if (!mHasMaxRadius) {
+ mTargetRadius = getTargetRadius(mBounds);
+ onTargetRadiusChanged(mTargetRadius);
+ }
+ }
+
+ public final void setup(float maxRadius, int densityDpi) {
+ if (maxRadius >= 0) {
+ mHasMaxRadius = true;
+ mTargetRadius = maxRadius;
+ } else {
+ mTargetRadius = getTargetRadius(mBounds);
+ }
+
+ mDensityScale = densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+
+ onTargetRadiusChanged(mTargetRadius);
+ }
+
+ private static float getTargetRadius(Rect bounds) {
+ final float halfWidth = bounds.width() / 2.0f;
+ final float halfHeight = bounds.height() / 2.0f;
+ return (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
+ }
+
+ /**
+ * Starts a ripple enter animation.
+ *
+ * @param fast whether the ripple should enter quickly
+ */
+ public final void enter(boolean fast) {
+ cancel();
+
+ mSoftwareAnimator = createSoftwareEnter(fast);
+
+ if (mSoftwareAnimator != null) {
+ mSoftwareAnimator.start();
+ }
+ }
+
+ /**
+ * Starts a ripple exit animation.
+ */
+ public final void exit() {
+ cancel();
+
+ if (mHasDisplayListCanvas) {
+ // We don't have access to a canvas here, but we expect one on the
+ // next frame. We'll start the render thread animation then.
+ mHasPendingHardwareAnimator = true;
+
+ // Request another frame.
+ invalidateSelf();
+ } else {
+ mSoftwareAnimator = createSoftwareExit();
+ mSoftwareAnimator.start();
+ }
+ }
+
+ /**
+ * Cancels all animations. Software animation values are left in the
+ * current state, while hardware animation values jump to the end state.
+ */
+ public void cancel() {
+ cancelSoftwareAnimations();
+ endHardwareAnimations();
+ }
+
+ /**
+ * Ends all animations, jumping values to the end state.
+ */
+ public void end() {
+ endSoftwareAnimations();
+ endHardwareAnimations();
+ }
+
+ /**
+ * Draws the ripple to the canvas, inheriting the paint's color and alpha
+ * properties.
+ *
+ * @param c the canvas to which the ripple should be drawn
+ * @param p the paint used to draw the ripple
+ * @return {@code true} if something was drawn, {@code false} otherwise
+ */
+ public boolean draw(Canvas c, Paint p) {
+ final boolean hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated()
+ && c instanceof DisplayListCanvas;
+ if (mHasDisplayListCanvas != hasDisplayListCanvas) {
+ mHasDisplayListCanvas = hasDisplayListCanvas;
+
+ if (!hasDisplayListCanvas) {
+ // We've switched from hardware to non-hardware mode. Panic.
+ endHardwareAnimations();
+ }
+ }
+
+ if (hasDisplayListCanvas) {
+ final DisplayListCanvas hw = (DisplayListCanvas) c;
+ startPendingAnimation(hw, p);
+
+ if (mHardwareAnimator != null) {
+ return drawHardware(hw);
+ }
+ }
+
+ return drawSoftware(c, p);
+ }
+
+ /**
+ * Populates {@code bounds} with the maximum drawing bounds of the ripple
+ * relative to its center. The resulting bounds should be translated into
+ * parent drawable coordinates before use.
+ *
+ * @param bounds the rect to populate with drawing bounds
+ */
+ public void getBounds(Rect bounds) {
+ final int r = (int) Math.ceil(mTargetRadius);
+ bounds.set(-r, -r, r, r);
+ }
+
+ /**
+ * Starts the pending hardware animation, if available.
+ *
+ * @param hw hardware canvas on which the animation should draw
+ * @param p paint whose properties the hardware canvas should use
+ */
+ private void startPendingAnimation(DisplayListCanvas hw, Paint p) {
+ if (mHasPendingHardwareAnimator) {
+ mHasPendingHardwareAnimator = false;
+
+ mHardwareAnimator = createHardwareExit(new Paint(p));
+ mHardwareAnimator.start(hw);
+
+ // Preemptively jump the software values to the end state now that
+ // the hardware exit has read whatever values it needs.
+ jumpValuesToExit();
+ }
+ }
+
+ /**
+ * Cancels any current software animations, leaving the values in their
+ * current state.
+ */
+ private void cancelSoftwareAnimations() {
+ if (mSoftwareAnimator != null) {
+ mSoftwareAnimator.cancel();
+ mSoftwareAnimator = null;
+ }
+ }
+
+ /**
+ * Ends any current software animations, jumping the values to their end
+ * state.
+ */
+ private void endSoftwareAnimations() {
+ if (mSoftwareAnimator != null) {
+ mSoftwareAnimator.end();
+ mSoftwareAnimator = null;
+ }
+ }
+
+ /**
+ * Ends any pending or current hardware animations.
+ * <p>
+ * Hardware animations can't synchronize values back to the software
+ * thread, so there is no "cancel" equivalent.
+ */
+ private void endHardwareAnimations() {
+ if (mHardwareAnimator != null) {
+ mHardwareAnimator.end();
+ mHardwareAnimator = null;
+ }
+
+ if (mHasPendingHardwareAnimator) {
+ mHasPendingHardwareAnimator = false;
+
+ // Manually jump values to their exited state. Normally we'd do that
+ // later when starting the hardware exit, but we're aborting early.
+ jumpValuesToExit();
+ }
+ }
+
+ protected final void invalidateSelf() {
+ mOwner.invalidateSelf(false);
+ }
+
+ protected final boolean isHardwareAnimating() {
+ return mHardwareAnimator != null && mHardwareAnimator.isRunning()
+ || mHasPendingHardwareAnimator;
+ }
+
+ protected final void onHotspotBoundsChanged() {
+ if (!mHasMaxRadius) {
+ final float halfWidth = mBounds.width() / 2.0f;
+ final float halfHeight = mBounds.height() / 2.0f;
+ final float targetRadius = (float) Math.sqrt(halfWidth * halfWidth
+ + halfHeight * halfHeight);
+
+ onTargetRadiusChanged(targetRadius);
+ }
+ }
+
+ /**
+ * Called when the target radius changes.
+ *
+ * @param targetRadius the new target radius
+ */
+ protected void onTargetRadiusChanged(float targetRadius) {
+ // Stub.
+ }
+
+ protected abstract Animator createSoftwareEnter(boolean fast);
+
+ protected abstract Animator createSoftwareExit();
+
+ protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p);
+
+ protected abstract boolean drawHardware(DisplayListCanvas c);
+
+ protected abstract boolean drawSoftware(Canvas c, Paint p);
+
+ /**
+ * Called when the hardware exit is cancelled. Jumps software values to end
+ * state to ensure that software and hardware values are synchronized.
+ */
+ protected abstract void jumpValuesToExit();
+
+ public static class RenderNodeAnimatorSet {
+ private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>();
+
+ public void add(RenderNodeAnimator anim) {
+ mAnimators.add(anim);
+ }
+
+ public void clear() {
+ mAnimators.clear();
+ }
+
+ public void start(DisplayListCanvas target) {
+ if (target == null) {
+ throw new IllegalArgumentException("Hardware canvas must be non-null");
+ }
+
+ final ArrayList<RenderNodeAnimator> animators = mAnimators;
+ final int N = animators.size();
+ for (int i = 0; i < N; i++) {
+ final RenderNodeAnimator anim = animators.get(i);
+ anim.setTarget(target);
+ anim.start();
+ }
+ }
+
+ public void cancel() {
+ final ArrayList<RenderNodeAnimator> animators = mAnimators;
+ final int N = animators.size();
+ for (int i = 0; i < N; i++) {
+ final RenderNodeAnimator anim = animators.get(i);
+ anim.cancel();
+ }
+ }
+
+ public void end() {
+ final ArrayList<RenderNodeAnimator> animators = mAnimators;
+ final int N = animators.size();
+ for (int i = 0; i < N; i++) {
+ final RenderNodeAnimator anim = animators.get(i);
+ anim.end();
+ }
+ }
+
+ public boolean isRunning() {
+ final ArrayList<RenderNodeAnimator> animators = mAnimators;
+ final int N = animators.size();
+ for (int i = 0; i < N; i++) {
+ final RenderNodeAnimator anim = animators.get(i);
+ if (anim.isRunning()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/android/graphics/drawable/RippleDrawable.java b/android/graphics/drawable/RippleDrawable.java
new file mode 100644
index 0000000..8f314c9
--- /dev/null
+++ b/android/graphics/drawable/RippleDrawable.java
@@ -0,0 +1,1066 @@
+/*
+ * Copyright (C) 2014 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 android.graphics.drawable;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Drawable that shows a ripple effect in response to state changes. The
+ * anchoring position of the ripple for a given state may be specified by
+ * calling {@link #setHotspot(float, float)} with the corresponding state
+ * attribute identifier.
+ * <p>
+ * A touch feedback drawable may contain multiple child layers, including a
+ * special mask layer that is not drawn to the screen. A single layer may be
+ * set as the mask from XML by specifying its {@code android:id} value as
+ * {@link android.R.id#mask}. At run time, a single layer may be set as the
+ * mask using {@code setId(..., android.R.id.mask)} or an existing mask layer
+ * may be replaced using {@code setDrawableByLayerId(android.R.id.mask, ...)}.
+ * <pre>
+ * <code><!-- A red ripple masked against an opaque rectangle. --/>
+ * <ripple android:color="#ffff0000">
+ * <item android:id="@android:id/mask"
+ * android:drawable="@android:color/white" />
+ * </ripple></code>
+ * </pre>
+ * <p>
+ * If a mask layer is set, the ripple effect will be masked against that layer
+ * before it is drawn over the composite of the remaining child layers.
+ * <p>
+ * If no mask layer is set, the ripple effect is masked against the composite
+ * of the child layers.
+ * <pre>
+ * <code><!-- A green ripple drawn atop a black rectangle. --/>
+ * <ripple android:color="#ff00ff00">
+ * <item android:drawable="@android:color/black" />
+ * </ripple>
+ *
+ * <!-- A blue ripple drawn atop a drawable resource. --/>
+ * <ripple android:color="#ff0000ff">
+ * <item android:drawable="@drawable/my_drawable" />
+ * </ripple></code>
+ * </pre>
+ * <p>
+ * If no child layers or mask is specified and the ripple is set as a View
+ * background, the ripple will be drawn atop the first available parent
+ * background within the View's hierarchy. In this case, the drawing region
+ * may extend outside of the Drawable bounds.
+ * <pre>
+ * <code><!-- An unbounded red ripple. --/>
+ * <ripple android:color="#ffff0000" /></code>
+ * </pre>
+ *
+ * @attr ref android.R.styleable#RippleDrawable_color
+ */
+public class RippleDrawable extends LayerDrawable {
+ /**
+ * Radius value that specifies the ripple radius should be computed based
+ * on the size of the ripple's container.
+ */
+ public static final int RADIUS_AUTO = -1;
+
+ private static final int MASK_UNKNOWN = -1;
+ private static final int MASK_NONE = 0;
+ private static final int MASK_CONTENT = 1;
+ private static final int MASK_EXPLICIT = 2;
+
+ /** The maximum number of ripples supported. */
+ private static final int MAX_RIPPLES = 10;
+
+ private final Rect mTempRect = new Rect();
+
+ /** Current ripple effect bounds, used to constrain ripple effects. */
+ private final Rect mHotspotBounds = new Rect();
+
+ /** Current drawing bounds, used to compute dirty region. */
+ private final Rect mDrawingBounds = new Rect();
+
+ /** Current dirty bounds, union of current and previous drawing bounds. */
+ private final Rect mDirtyBounds = new Rect();
+
+ /** Mirrors mLayerState with some extra information. */
+ private RippleState mState;
+
+ /** The masking layer, e.g. the layer with id R.id.mask. */
+ private Drawable mMask;
+
+ /** The current background. May be actively animating or pending entry. */
+ private RippleBackground mBackground;
+
+ private Bitmap mMaskBuffer;
+ private BitmapShader mMaskShader;
+ private Canvas mMaskCanvas;
+ private Matrix mMaskMatrix;
+ private PorterDuffColorFilter mMaskColorFilter;
+ private boolean mHasValidMask;
+
+ /** Whether we expect to draw a background when visible. */
+ private boolean mBackgroundActive;
+
+ /** The current ripple. May be actively animating or pending entry. */
+ private RippleForeground mRipple;
+
+ /** Whether we expect to draw a ripple when visible. */
+ private boolean mRippleActive;
+
+ // Hotspot coordinates that are awaiting activation.
+ private float mPendingX;
+ private float mPendingY;
+ private boolean mHasPending;
+
+ /**
+ * Lazily-created array of actively animating ripples. Inactive ripples are
+ * pruned during draw(). The locations of these will not change.
+ */
+ private RippleForeground[] mExitingRipples;
+ private int mExitingRipplesCount = 0;
+
+ /** Paint used to control appearance of ripples. */
+ private Paint mRipplePaint;
+
+ /** Target density of the display into which ripples are drawn. */
+ private int mDensity;
+
+ /** Whether bounds are being overridden. */
+ private boolean mOverrideBounds;
+
+ /**
+ * If set, force all ripple animations to not run on RenderThread, even if it would be
+ * available.
+ */
+ private boolean mForceSoftware;
+
+ /**
+ * Constructor used for drawable inflation.
+ */
+ RippleDrawable() {
+ this(new RippleState(null, null, null), null);
+ }
+
+ /**
+ * Creates a new ripple drawable with the specified ripple color and
+ * optional content and mask drawables.
+ *
+ * @param color The ripple color
+ * @param content The content drawable, may be {@code null}
+ * @param mask The mask drawable, may be {@code null}
+ */
+ public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content,
+ @Nullable Drawable mask) {
+ this(new RippleState(null, null, null), null);
+
+ if (color == null) {
+ throw new IllegalArgumentException("RippleDrawable requires a non-null color");
+ }
+
+ if (content != null) {
+ addLayer(content, null, 0, 0, 0, 0, 0);
+ }
+
+ if (mask != null) {
+ addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0);
+ }
+
+ setColor(color);
+ ensurePadding();
+ refreshPadding();
+ updateLocalState();
+ }
+
+ @Override
+ public void jumpToCurrentState() {
+ super.jumpToCurrentState();
+
+ if (mRipple != null) {
+ mRipple.end();
+ }
+
+ if (mBackground != null) {
+ mBackground.end();
+ }
+
+ cancelExitingRipples();
+ }
+
+ private void cancelExitingRipples() {
+ final int count = mExitingRipplesCount;
+ final RippleForeground[] ripples = mExitingRipples;
+ for (int i = 0; i < count; i++) {
+ ripples[i].end();
+ }
+
+ if (ripples != null) {
+ Arrays.fill(ripples, 0, count, null);
+ }
+ mExitingRipplesCount = 0;
+
+ // Always draw an additional "clean" frame after canceling animations.
+ invalidateSelf(false);
+ }
+
+ @Override
+ public int getOpacity() {
+ // Worst-case scenario.
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ protected boolean onStateChange(int[] stateSet) {
+ final boolean changed = super.onStateChange(stateSet);
+
+ boolean enabled = false;
+ boolean pressed = false;
+ boolean focused = false;
+ boolean hovered = false;
+
+ for (int state : stateSet) {
+ if (state == R.attr.state_enabled) {
+ enabled = true;
+ } else if (state == R.attr.state_focused) {
+ focused = true;
+ } else if (state == R.attr.state_pressed) {
+ pressed = true;
+ } else if (state == R.attr.state_hovered) {
+ hovered = true;
+ }
+ }
+
+ setRippleActive(enabled && pressed);
+ setBackgroundActive(hovered || focused || (enabled && pressed), focused || hovered);
+
+ return changed;
+ }
+
+ private void setRippleActive(boolean active) {
+ if (mRippleActive != active) {
+ mRippleActive = active;
+ if (active) {
+ tryRippleEnter();
+ } else {
+ tryRippleExit();
+ }
+ }
+ }
+
+ private void setBackgroundActive(boolean active, boolean focused) {
+ if (mBackgroundActive != active) {
+ mBackgroundActive = active;
+ if (active) {
+ tryBackgroundEnter(focused);
+ } else {
+ tryBackgroundExit();
+ }
+ }
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+
+ if (!mOverrideBounds) {
+ mHotspotBounds.set(bounds);
+ onHotspotBoundsChanged();
+ }
+
+ if (mBackground != null) {
+ mBackground.onBoundsChange();
+ }
+
+ if (mRipple != null) {
+ mRipple.onBoundsChange();
+ }
+
+ invalidateSelf();
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ final boolean changed = super.setVisible(visible, restart);
+
+ if (!visible) {
+ clearHotspots();
+ } else if (changed) {
+ // If we just became visible, ensure the background and ripple
+ // visibilities are consistent with their internal states.
+ if (mRippleActive) {
+ tryRippleEnter();
+ }
+
+ if (mBackgroundActive) {
+ tryBackgroundEnter(false);
+ }
+
+ // Skip animations, just show the correct final states.
+ jumpToCurrentState();
+ }
+
+ return changed;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean isProjected() {
+ // If the layer is bounded, then we don't need to project.
+ if (isBounded()) {
+ return false;
+ }
+
+ // Otherwise, if the maximum radius is contained entirely within the
+ // bounds then we don't need to project. This is sort of a hack to
+ // prevent check box ripples from being projected across the edges of
+ // scroll views. It does not impact rendering performance, and it can
+ // be removed once we have better handling of projection in scrollable
+ // views.
+ final int radius = mState.mMaxRadius;
+ final Rect drawableBounds = getBounds();
+ final Rect hotspotBounds = mHotspotBounds;
+ if (radius != RADIUS_AUTO
+ && radius <= hotspotBounds.width() / 2
+ && radius <= hotspotBounds.height() / 2
+ && (drawableBounds.equals(hotspotBounds)
+ || drawableBounds.contains(hotspotBounds))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean isBounded() {
+ return getNumberOfLayers() > 0;
+ }
+
+ @Override
+ public boolean isStateful() {
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return true;
+ }
+
+ /**
+ * Sets the ripple color.
+ *
+ * @param color Ripple color as a color state list.
+ *
+ * @attr ref android.R.styleable#RippleDrawable_color
+ */
+ public void setColor(ColorStateList color) {
+ mState.mColor = color;
+ invalidateSelf(false);
+ }
+
+ /**
+ * Sets the radius in pixels of the fully expanded ripple.
+ *
+ * @param radius ripple radius in pixels, or {@link #RADIUS_AUTO} to
+ * compute the radius based on the container size
+ * @attr ref android.R.styleable#RippleDrawable_radius
+ */
+ public void setRadius(int radius) {
+ mState.mMaxRadius = radius;
+ invalidateSelf(false);
+ }
+
+ /**
+ * @return the radius in pixels of the fully expanded ripple if an explicit
+ * radius has been set, or {@link #RADIUS_AUTO} if the radius is
+ * computed based on the container size
+ * @attr ref android.R.styleable#RippleDrawable_radius
+ */
+ public int getRadius() {
+ return mState.mMaxRadius;
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
+
+ // Force padding default to STACK before inflating.
+ setPaddingMode(PADDING_MODE_STACK);
+
+ // Inflation will advance the XmlPullParser and AttributeSet.
+ super.inflate(r, parser, attrs, theme);
+
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ a.recycle();
+
+ updateLocalState();
+ }
+
+ @Override
+ public boolean setDrawableByLayerId(int id, Drawable drawable) {
+ if (super.setDrawableByLayerId(id, drawable)) {
+ if (id == R.id.mask) {
+ mMask = drawable;
+ mHasValidMask = false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Specifies how layer padding should affect the bounds of subsequent
+ * layers. The default and recommended value for RippleDrawable is
+ * {@link #PADDING_MODE_STACK}.
+ *
+ * @param mode padding mode, one of:
+ * <ul>
+ * <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
+ * padding of the previous layer
+ * <li>{@link #PADDING_MODE_STACK} to stack each layer directly
+ * atop the previous layer
+ * </ul>
+ * @see #getPaddingMode()
+ */
+ @Override
+ public void setPaddingMode(int mode) {
+ super.setPaddingMode(mode);
+ }
+
+ /**
+ * Initializes the constant state from the values in the typed array.
+ */
+ private void updateStateFromTypedArray(@NonNull TypedArray a) throws XmlPullParserException {
+ final RippleState state = mState;
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mTouchThemeAttrs = a.extractThemeAttrs();
+
+ final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
+ if (color != null) {
+ mState.mColor = color;
+ }
+
+ mState.mMaxRadius = a.getDimensionPixelSize(
+ R.styleable.RippleDrawable_radius, mState.mMaxRadius);
+ }
+
+ private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
+ if (mState.mColor == null && (mState.mTouchThemeAttrs == null
+ || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
+ throw new XmlPullParserException(a.getPositionDescription() +
+ ": <ripple> requires a valid color attribute");
+ }
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ final RippleState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ if (state.mTouchThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
+ R.styleable.RippleDrawable);
+ try {
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ } catch (XmlPullParserException e) {
+ rethrowAsRuntimeException(e);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ if (state.mColor != null && state.mColor.canApplyTheme()) {
+ state.mColor = state.mColor.obtainForTheme(t);
+ }
+
+ updateLocalState();
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
+ }
+
+ @Override
+ public void setHotspot(float x, float y) {
+ if (mRipple == null || mBackground == null) {
+ mPendingX = x;
+ mPendingY = y;
+ mHasPending = true;
+ }
+
+ if (mRipple != null) {
+ mRipple.move(x, y);
+ }
+ }
+
+ /**
+ * Creates an active hotspot at the specified location.
+ */
+ private void tryBackgroundEnter(boolean focused) {
+ if (mBackground == null) {
+ final boolean isBounded = isBounded();
+ mBackground = new RippleBackground(this, mHotspotBounds, isBounded, mForceSoftware);
+ }
+
+ mBackground.setup(mState.mMaxRadius, mDensity);
+ mBackground.enter(focused);
+ }
+
+ private void tryBackgroundExit() {
+ if (mBackground != null) {
+ // Don't null out the background, we need it to draw!
+ mBackground.exit();
+ }
+ }
+
+ /**
+ * Attempts to start an enter animation for the active hotspot. Fails if
+ * there are too many animating ripples.
+ */
+ private void tryRippleEnter() {
+ if (mExitingRipplesCount >= MAX_RIPPLES) {
+ // This should never happen unless the user is tapping like a maniac
+ // or there is a bug that's preventing ripples from being removed.
+ return;
+ }
+
+ if (mRipple == null) {
+ final float x;
+ final float y;
+ if (mHasPending) {
+ mHasPending = false;
+ x = mPendingX;
+ y = mPendingY;
+ } else {
+ x = mHotspotBounds.exactCenterX();
+ y = mHotspotBounds.exactCenterY();
+ }
+
+ final boolean isBounded = isBounded();
+ mRipple = new RippleForeground(this, mHotspotBounds, x, y, isBounded, mForceSoftware);
+ }
+
+ mRipple.setup(mState.mMaxRadius, mDensity);
+ mRipple.enter(false);
+ }
+
+ /**
+ * Attempts to start an exit animation for the active hotspot. Fails if
+ * there is no active hotspot.
+ */
+ private void tryRippleExit() {
+ if (mRipple != null) {
+ if (mExitingRipples == null) {
+ mExitingRipples = new RippleForeground[MAX_RIPPLES];
+ }
+ mExitingRipples[mExitingRipplesCount++] = mRipple;
+ mRipple.exit();
+ mRipple = null;
+ }
+ }
+
+ /**
+ * Cancels and removes the active ripple, all exiting ripples, and the
+ * background. Nothing will be drawn after this method is called.
+ */
+ private void clearHotspots() {
+ if (mRipple != null) {
+ mRipple.end();
+ mRipple = null;
+ mRippleActive = false;
+ }
+
+ if (mBackground != null) {
+ mBackground.end();
+ mBackground = null;
+ mBackgroundActive = false;
+ }
+
+ cancelExitingRipples();
+ }
+
+ @Override
+ public void setHotspotBounds(int left, int top, int right, int bottom) {
+ mOverrideBounds = true;
+ mHotspotBounds.set(left, top, right, bottom);
+
+ onHotspotBoundsChanged();
+ }
+
+ @Override
+ public void getHotspotBounds(Rect outRect) {
+ outRect.set(mHotspotBounds);
+ }
+
+ /**
+ * Notifies all the animating ripples that the hotspot bounds have changed.
+ */
+ private void onHotspotBoundsChanged() {
+ final int count = mExitingRipplesCount;
+ final RippleForeground[] ripples = mExitingRipples;
+ for (int i = 0; i < count; i++) {
+ ripples[i].onHotspotBoundsChanged();
+ }
+
+ if (mRipple != null) {
+ mRipple.onHotspotBoundsChanged();
+ }
+
+ if (mBackground != null) {
+ mBackground.onHotspotBoundsChanged();
+ }
+ }
+
+ /**
+ * Populates <code>outline</code> with the first available layer outline,
+ * excluding the mask layer.
+ *
+ * @param outline Outline in which to place the first available layer outline
+ */
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ final LayerState state = mLayerState;
+ final ChildDrawable[] children = state.mChildren;
+ final int N = state.mNumChildren;
+ for (int i = 0; i < N; i++) {
+ if (children[i].mId != R.id.mask) {
+ children[i].mDrawable.getOutline(outline);
+ if (!outline.isEmpty()) return;
+ }
+ }
+ }
+
+ /**
+ * Optimized for drawing ripples with a mask layer and optional content.
+ */
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ pruneRipples();
+
+ // Clip to the dirty bounds, which will be the drawable bounds if we
+ // have a mask or content and the ripple bounds if we're projecting.
+ final Rect bounds = getDirtyBounds();
+ final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
+ canvas.clipRect(bounds);
+
+ drawContent(canvas);
+ drawBackgroundAndRipples(canvas);
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ public void invalidateSelf() {
+ invalidateSelf(true);
+ }
+
+ void invalidateSelf(boolean invalidateMask) {
+ super.invalidateSelf();
+
+ if (invalidateMask) {
+ // Force the mask to update on the next draw().
+ mHasValidMask = false;
+ }
+
+ }
+
+ private void pruneRipples() {
+ int remaining = 0;
+
+ // Move remaining entries into pruned spaces.
+ final RippleForeground[] ripples = mExitingRipples;
+ final int count = mExitingRipplesCount;
+ for (int i = 0; i < count; i++) {
+ if (!ripples[i].hasFinishedExit()) {
+ ripples[remaining++] = ripples[i];
+ }
+ }
+
+ // Null out the remaining entries.
+ for (int i = remaining; i < count; i++) {
+ ripples[i] = null;
+ }
+
+ mExitingRipplesCount = remaining;
+ }
+
+ /**
+ * @return whether we need to use a mask
+ */
+ private void updateMaskShaderIfNeeded() {
+ if (mHasValidMask) {
+ return;
+ }
+
+ final int maskType = getMaskType();
+ if (maskType == MASK_UNKNOWN) {
+ return;
+ }
+
+ mHasValidMask = true;
+
+ final Rect bounds = getBounds();
+ if (maskType == MASK_NONE || bounds.isEmpty()) {
+ if (mMaskBuffer != null) {
+ mMaskBuffer.recycle();
+ mMaskBuffer = null;
+ mMaskShader = null;
+ mMaskCanvas = null;
+ }
+ mMaskMatrix = null;
+ mMaskColorFilter = null;
+ return;
+ }
+
+ // Ensure we have a correctly-sized buffer.
+ if (mMaskBuffer == null
+ || mMaskBuffer.getWidth() != bounds.width()
+ || mMaskBuffer.getHeight() != bounds.height()) {
+ if (mMaskBuffer != null) {
+ mMaskBuffer.recycle();
+ }
+
+ mMaskBuffer = Bitmap.createBitmap(
+ bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
+ mMaskShader = new BitmapShader(mMaskBuffer,
+ Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+ mMaskCanvas = new Canvas(mMaskBuffer);
+ } else {
+ mMaskBuffer.eraseColor(Color.TRANSPARENT);
+ }
+
+ if (mMaskMatrix == null) {
+ mMaskMatrix = new Matrix();
+ } else {
+ mMaskMatrix.reset();
+ }
+
+ if (mMaskColorFilter == null) {
+ mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
+ }
+
+ // Draw the appropriate mask anchored to (0,0).
+ final int left = bounds.left;
+ final int top = bounds.top;
+ mMaskCanvas.translate(-left, -top);
+ if (maskType == MASK_EXPLICIT) {
+ drawMask(mMaskCanvas);
+ } else if (maskType == MASK_CONTENT) {
+ drawContent(mMaskCanvas);
+ }
+ mMaskCanvas.translate(left, top);
+ }
+
+ private int getMaskType() {
+ if (mRipple == null && mExitingRipplesCount <= 0
+ && (mBackground == null || !mBackground.isVisible())) {
+ // We might need a mask later.
+ return MASK_UNKNOWN;
+ }
+
+ if (mMask != null) {
+ if (mMask.getOpacity() == PixelFormat.OPAQUE) {
+ // Clipping handles opaque explicit masks.
+ return MASK_NONE;
+ } else {
+ return MASK_EXPLICIT;
+ }
+ }
+
+ // Check for non-opaque, non-mask content.
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int count = mLayerState.mNumChildren;
+ for (int i = 0; i < count; i++) {
+ if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
+ return MASK_CONTENT;
+ }
+ }
+
+ // Clipping handles opaque content.
+ return MASK_NONE;
+ }
+
+ private void drawContent(Canvas canvas) {
+ // Draw everything except the mask.
+ final ChildDrawable[] array = mLayerState.mChildren;
+ final int count = mLayerState.mNumChildren;
+ for (int i = 0; i < count; i++) {
+ if (array[i].mId != R.id.mask) {
+ array[i].mDrawable.draw(canvas);
+ }
+ }
+ }
+
+ private void drawBackgroundAndRipples(Canvas canvas) {
+ final RippleForeground active = mRipple;
+ final RippleBackground background = mBackground;
+ final int count = mExitingRipplesCount;
+ if (active == null && count <= 0 && (background == null || !background.isVisible())) {
+ // Move along, nothing to draw here.
+ return;
+ }
+
+ final float x = mHotspotBounds.exactCenterX();
+ final float y = mHotspotBounds.exactCenterY();
+ canvas.translate(x, y);
+
+ updateMaskShaderIfNeeded();
+
+ // Position the shader to account for canvas translation.
+ if (mMaskShader != null) {
+ final Rect bounds = getBounds();
+ mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
+ mMaskShader.setLocalMatrix(mMaskMatrix);
+ }
+
+ // Grab the color for the current state and cut the alpha channel in
+ // half so that the ripple and background together yield full alpha.
+ final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
+ final int halfAlpha = (Color.alpha(color) / 2) << 24;
+ final Paint p = getRipplePaint();
+
+ if (mMaskColorFilter != null) {
+ // The ripple timing depends on the paint's alpha value, so we need
+ // to push just the alpha channel into the paint and let the filter
+ // handle the full-alpha color.
+ final int fullAlphaColor = color | (0xFF << 24);
+ mMaskColorFilter.setColor(fullAlphaColor);
+
+ p.setColor(halfAlpha);
+ p.setColorFilter(mMaskColorFilter);
+ p.setShader(mMaskShader);
+ } else {
+ final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
+ p.setColor(halfAlphaColor);
+ p.setColorFilter(null);
+ p.setShader(null);
+ }
+
+ if (background != null && background.isVisible()) {
+ background.draw(canvas, p);
+ }
+
+ if (count > 0) {
+ final RippleForeground[] ripples = mExitingRipples;
+ for (int i = 0; i < count; i++) {
+ ripples[i].draw(canvas, p);
+ }
+ }
+
+ if (active != null) {
+ active.draw(canvas, p);
+ }
+
+ canvas.translate(-x, -y);
+ }
+
+ private void drawMask(Canvas canvas) {
+ mMask.draw(canvas);
+ }
+
+ private Paint getRipplePaint() {
+ if (mRipplePaint == null) {
+ mRipplePaint = new Paint();
+ mRipplePaint.setAntiAlias(true);
+ mRipplePaint.setStyle(Paint.Style.FILL);
+ }
+ return mRipplePaint;
+ }
+
+ @Override
+ public Rect getDirtyBounds() {
+ if (!isBounded()) {
+ final Rect drawingBounds = mDrawingBounds;
+ final Rect dirtyBounds = mDirtyBounds;
+ dirtyBounds.set(drawingBounds);
+ drawingBounds.setEmpty();
+
+ final int cX = (int) mHotspotBounds.exactCenterX();
+ final int cY = (int) mHotspotBounds.exactCenterY();
+ final Rect rippleBounds = mTempRect;
+
+ final RippleForeground[] activeRipples = mExitingRipples;
+ final int N = mExitingRipplesCount;
+ for (int i = 0; i < N; i++) {
+ activeRipples[i].getBounds(rippleBounds);
+ rippleBounds.offset(cX, cY);
+ drawingBounds.union(rippleBounds);
+ }
+
+ final RippleBackground background = mBackground;
+ if (background != null) {
+ background.getBounds(rippleBounds);
+ rippleBounds.offset(cX, cY);
+ drawingBounds.union(rippleBounds);
+ }
+
+ dirtyBounds.union(drawingBounds);
+ dirtyBounds.union(super.getDirtyBounds());
+ return dirtyBounds;
+ } else {
+ return getBounds();
+ }
+ }
+
+ /**
+ * Sets whether to disable RenderThread animations for this ripple.
+ *
+ * @param forceSoftware true if RenderThread animations should be disabled, false otherwise
+ * @hide
+ */
+ public void setForceSoftware(boolean forceSoftware) {
+ mForceSoftware = forceSoftware;
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ return mState;
+ }
+
+ @Override
+ public Drawable mutate() {
+ super.mutate();
+
+ // LayerDrawable creates a new state using createConstantState, so
+ // this should always be a safe cast.
+ mState = (RippleState) mLayerState;
+
+ // The locally cached drawable may have changed.
+ mMask = findDrawableByLayerId(R.id.mask);
+
+ return this;
+ }
+
+ @Override
+ RippleState createConstantState(LayerState state, Resources res) {
+ return new RippleState(state, this, res);
+ }
+
+ static class RippleState extends LayerState {
+ int[] mTouchThemeAttrs;
+ ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
+ int mMaxRadius = RADIUS_AUTO;
+
+ public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
+ super(orig, owner, res);
+
+ if (orig != null && orig instanceof RippleState) {
+ final RippleState origs = (RippleState) orig;
+ mTouchThemeAttrs = origs.mTouchThemeAttrs;
+ mColor = origs.mColor;
+ mMaxRadius = origs.mMaxRadius;
+
+ if (origs.mDensity != mDensity) {
+ applyDensityScaling(orig.mDensity, mDensity);
+ }
+ }
+ }
+
+ @Override
+ protected void onDensityChanged(int sourceDensity, int targetDensity) {
+ super.onDensityChanged(sourceDensity, targetDensity);
+
+ applyDensityScaling(sourceDensity, targetDensity);
+ }
+
+ private void applyDensityScaling(int sourceDensity, int targetDensity) {
+ if (mMaxRadius != RADIUS_AUTO) {
+ mMaxRadius = Drawable.scaleFromDensity(
+ mMaxRadius, sourceDensity, targetDensity, true);
+ }
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mTouchThemeAttrs != null
+ || (mColor != null && mColor.canApplyTheme())
+ || super.canApplyTheme();
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new RippleDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new RippleDrawable(this, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations()
+ | (mColor != null ? mColor.getChangingConfigurations() : 0);
+ }
+ }
+
+ private RippleDrawable(RippleState state, Resources res) {
+ mState = new RippleState(state, this, res);
+ mLayerState = mState;
+ mDensity = Drawable.resolveDensity(res, mState.mDensity);
+
+ if (mState.mNumChildren > 0) {
+ ensurePadding();
+ refreshPadding();
+ }
+
+ updateLocalState();
+ }
+
+ private void updateLocalState() {
+ // Initialize from constant state.
+ mMask = findDrawableByLayerId(R.id.mask);
+ }
+}
diff --git a/android/graphics/drawable/RippleForeground.java b/android/graphics/drawable/RippleForeground.java
new file mode 100644
index 0000000..829733e
--- /dev/null
+++ b/android/graphics/drawable/RippleForeground.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2015 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 android.graphics.drawable;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.FloatProperty;
+import android.util.MathUtils;
+import android.view.DisplayListCanvas;
+import android.view.RenderNodeAnimator;
+import android.view.animation.LinearInterpolator;
+
+/**
+ * Draws a ripple foreground.
+ */
+class RippleForeground extends RippleComponent {
+ private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+ private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator(
+ 400f, 1.4f, 0);
+
+ // Pixel-based accelerations and velocities.
+ private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024;
+ private static final float WAVE_TOUCH_UP_ACCELERATION = 3400;
+ private static final float WAVE_OPACITY_DECAY_VELOCITY = 3;
+
+ // Bounded ripple animation properties.
+ private static final int BOUNDED_ORIGIN_EXIT_DURATION = 300;
+ private static final int BOUNDED_RADIUS_EXIT_DURATION = 800;
+ private static final int BOUNDED_OPACITY_EXIT_DURATION = 400;
+ private static final float MAX_BOUNDED_RADIUS = 350;
+
+ private static final int RIPPLE_ENTER_DELAY = 80;
+ private static final int OPACITY_ENTER_DURATION_FAST = 120;
+
+ // Parent-relative values for starting position.
+ private float mStartingX;
+ private float mStartingY;
+ private float mClampedStartingX;
+ private float mClampedStartingY;
+
+ // Hardware rendering properties.
+ private CanvasProperty<Paint> mPropPaint;
+ private CanvasProperty<Float> mPropRadius;
+ private CanvasProperty<Float> mPropX;
+ private CanvasProperty<Float> mPropY;
+
+ // Target values for tween animations.
+ private float mTargetX = 0;
+ private float mTargetY = 0;
+
+ /** Ripple target radius used when bounded. Not used for clamping. */
+ private float mBoundedRadius = 0;
+
+ // Software rendering properties.
+ private float mOpacity = 1;
+
+ // Values used to tween between the start and end positions.
+ private float mTweenRadius = 0;
+ private float mTweenX = 0;
+ private float mTweenY = 0;
+
+ /** Whether this ripple is bounded. */
+ private boolean mIsBounded;
+
+ /** Whether this ripple has finished its exit animation. */
+ private boolean mHasFinishedExit;
+
+ public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY,
+ boolean isBounded, boolean forceSoftware) {
+ super(owner, bounds, forceSoftware);
+
+ mIsBounded = isBounded;
+ mStartingX = startingX;
+ mStartingY = startingY;
+
+ if (isBounded) {
+ mBoundedRadius = MAX_BOUNDED_RADIUS * 0.9f
+ + (float) (MAX_BOUNDED_RADIUS * Math.random() * 0.1);
+ } else {
+ mBoundedRadius = 0;
+ }
+ }
+
+ @Override
+ protected void onTargetRadiusChanged(float targetRadius) {
+ clampStartingPosition();
+ }
+
+ @Override
+ protected boolean drawSoftware(Canvas c, Paint p) {
+ boolean hasContent = false;
+
+ final int origAlpha = p.getAlpha();
+ final int alpha = (int) (origAlpha * mOpacity + 0.5f);
+ final float radius = getCurrentRadius();
+ if (alpha > 0 && radius > 0) {
+ final float x = getCurrentX();
+ final float y = getCurrentY();
+ p.setAlpha(alpha);
+ c.drawCircle(x, y, radius, p);
+ p.setAlpha(origAlpha);
+ hasContent = true;
+ }
+
+ return hasContent;
+ }
+
+ @Override
+ protected boolean drawHardware(DisplayListCanvas c) {
+ c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
+ return true;
+ }
+
+ /**
+ * Returns the maximum bounds of the ripple relative to the ripple center.
+ */
+ public void getBounds(Rect bounds) {
+ final int outerX = (int) mTargetX;
+ final int outerY = (int) mTargetY;
+ final int r = (int) mTargetRadius + 1;
+ bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
+ }
+
+ /**
+ * Specifies the starting position relative to the drawable bounds. No-op if
+ * the ripple has already entered.
+ */
+ public void move(float x, float y) {
+ mStartingX = x;
+ mStartingY = y;
+
+ clampStartingPosition();
+ }
+
+ /**
+ * @return {@code true} if this ripple has finished its exit animation
+ */
+ public boolean hasFinishedExit() {
+ return mHasFinishedExit;
+ }
+
+ @Override
+ protected Animator createSoftwareEnter(boolean fast) {
+ // Bounded ripples don't have enter animations.
+ if (mIsBounded) {
+ return null;
+ }
+
+ final int duration = (int)
+ (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensityScale) + 0.5);
+
+ final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
+ tweenRadius.setAutoCancel(true);
+ tweenRadius.setDuration(duration);
+ tweenRadius.setInterpolator(LINEAR_INTERPOLATOR);
+ tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY);
+
+ final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
+ tweenOrigin.setAutoCancel(true);
+ tweenOrigin.setDuration(duration);
+ tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR);
+ tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY);
+
+ final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
+ opacity.setAutoCancel(true);
+ opacity.setDuration(OPACITY_ENTER_DURATION_FAST);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
+
+ final AnimatorSet set = new AnimatorSet();
+ set.play(tweenOrigin).with(tweenRadius).with(opacity);
+
+ return set;
+ }
+
+ private float getCurrentX() {
+ return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX);
+ }
+
+ private float getCurrentY() {
+ return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY);
+ }
+
+ private int getRadiusExitDuration() {
+ final float remainingRadius = mTargetRadius - getCurrentRadius();
+ return (int) (1000 * Math.sqrt(remainingRadius / (WAVE_TOUCH_UP_ACCELERATION
+ + WAVE_TOUCH_DOWN_ACCELERATION) * mDensityScale) + 0.5);
+ }
+
+ private float getCurrentRadius() {
+ return MathUtils.lerp(0, mTargetRadius, mTweenRadius);
+ }
+
+ private int getOpacityExitDuration() {
+ return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
+ }
+
+ /**
+ * Compute target values that are dependent on bounding.
+ */
+ private void computeBoundedTargetValues() {
+ mTargetX = (mClampedStartingX - mBounds.exactCenterX()) * .7f;
+ mTargetY = (mClampedStartingY - mBounds.exactCenterY()) * .7f;
+ mTargetRadius = mBoundedRadius;
+ }
+
+ @Override
+ protected Animator createSoftwareExit() {
+ final int radiusDuration;
+ final int originDuration;
+ final int opacityDuration;
+ if (mIsBounded) {
+ computeBoundedTargetValues();
+
+ radiusDuration = BOUNDED_RADIUS_EXIT_DURATION;
+ originDuration = BOUNDED_ORIGIN_EXIT_DURATION;
+ opacityDuration = BOUNDED_OPACITY_EXIT_DURATION;
+ } else {
+ radiusDuration = getRadiusExitDuration();
+ originDuration = radiusDuration;
+ opacityDuration = getOpacityExitDuration();
+ }
+
+ final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
+ tweenRadius.setAutoCancel(true);
+ tweenRadius.setDuration(radiusDuration);
+ tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
+
+ final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
+ tweenOrigin.setAutoCancel(true);
+ tweenOrigin.setDuration(originDuration);
+ tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR);
+
+ final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0);
+ opacity.setAutoCancel(true);
+ opacity.setDuration(opacityDuration);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
+
+ final AnimatorSet set = new AnimatorSet();
+ set.play(tweenOrigin).with(tweenRadius).with(opacity);
+ set.addListener(mAnimationListener);
+
+ return set;
+ }
+
+ @Override
+ protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
+ final int radiusDuration;
+ final int originDuration;
+ final int opacityDuration;
+ if (mIsBounded) {
+ computeBoundedTargetValues();
+
+ radiusDuration = BOUNDED_RADIUS_EXIT_DURATION;
+ originDuration = BOUNDED_ORIGIN_EXIT_DURATION;
+ opacityDuration = BOUNDED_OPACITY_EXIT_DURATION;
+ } else {
+ radiusDuration = getRadiusExitDuration();
+ originDuration = radiusDuration;
+ opacityDuration = getOpacityExitDuration();
+ }
+
+ final float startX = getCurrentX();
+ final float startY = getCurrentY();
+ final float startRadius = getCurrentRadius();
+
+ p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f));
+
+ mPropPaint = CanvasProperty.createPaint(p);
+ mPropRadius = CanvasProperty.createFloat(startRadius);
+ mPropX = CanvasProperty.createFloat(startX);
+ mPropY = CanvasProperty.createFloat(startY);
+
+ final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
+ radius.setDuration(radiusDuration);
+ radius.setInterpolator(DECELERATE_INTERPOLATOR);
+
+ final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX);
+ x.setDuration(originDuration);
+ x.setInterpolator(DECELERATE_INTERPOLATOR);
+
+ final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
+ y.setDuration(originDuration);
+ y.setInterpolator(DECELERATE_INTERPOLATOR);
+
+ final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
+ RenderNodeAnimator.PAINT_ALPHA, 0);
+ opacity.setDuration(opacityDuration);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
+ opacity.addListener(mAnimationListener);
+
+ final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
+ set.add(radius);
+ set.add(opacity);
+ set.add(x);
+ set.add(y);
+
+ return set;
+ }
+
+ @Override
+ protected void jumpValuesToExit() {
+ mOpacity = 0;
+ mTweenX = 1;
+ mTweenY = 1;
+ mTweenRadius = 1;
+ }
+
+ /**
+ * Clamps the starting position to fit within the ripple bounds.
+ */
+ private void clampStartingPosition() {
+ final float cX = mBounds.exactCenterX();
+ final float cY = mBounds.exactCenterY();
+ final float dX = mStartingX - cX;
+ final float dY = mStartingY - cY;
+ final float r = mTargetRadius;
+ if (dX * dX + dY * dY > r * r) {
+ // Point is outside the circle, clamp to the perimeter.
+ final double angle = Math.atan2(dY, dX);
+ mClampedStartingX = cX + (float) (Math.cos(angle) * r);
+ mClampedStartingY = cY + (float) (Math.sin(angle) * r);
+ } else {
+ mClampedStartingX = mStartingX;
+ mClampedStartingY = mStartingY;
+ }
+ }
+
+ private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mHasFinishedExit = true;
+ }
+ };
+
+ /**
+ * Interpolator with a smooth log deceleration.
+ */
+ private static final class LogDecelerateInterpolator implements TimeInterpolator {
+ private final float mBase;
+ private final float mDrift;
+ private final float mTimeScale;
+ private final float mOutputScale;
+
+ public LogDecelerateInterpolator(float base, float timeScale, float drift) {
+ mBase = base;
+ mDrift = drift;
+ mTimeScale = 1f / timeScale;
+
+ mOutputScale = 1f / computeLog(1f);
+ }
+
+ private float computeLog(float t) {
+ return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t);
+ }
+
+ @Override
+ public float getInterpolation(float t) {
+ return computeLog(t) * mOutputScale;
+ }
+ }
+
+ /**
+ * Property for animating radius between its initial and target values.
+ */
+ private static final FloatProperty<RippleForeground> TWEEN_RADIUS =
+ new FloatProperty<RippleForeground>("tweenRadius") {
+ @Override
+ public void setValue(RippleForeground object, float value) {
+ object.mTweenRadius = value;
+ object.invalidateSelf();
+ }
+
+ @Override
+ public Float get(RippleForeground object) {
+ return object.mTweenRadius;
+ }
+ };
+
+ /**
+ * Property for animating origin between its initial and target values.
+ */
+ private static final FloatProperty<RippleForeground> TWEEN_ORIGIN =
+ new FloatProperty<RippleForeground>("tweenOrigin") {
+ @Override
+ public void setValue(RippleForeground object, float value) {
+ object.mTweenX = value;
+ object.mTweenY = value;
+ object.invalidateSelf();
+ }
+
+ @Override
+ public Float get(RippleForeground object) {
+ return object.mTweenX;
+ }
+ };
+
+ /**
+ * Property for animating opacity between 0 and its target value.
+ */
+ private static final FloatProperty<RippleForeground> OPACITY =
+ new FloatProperty<RippleForeground>("opacity") {
+ @Override
+ public void setValue(RippleForeground object, float value) {
+ object.mOpacity = value;
+ object.invalidateSelf();
+ }
+
+ @Override
+ public Float get(RippleForeground object) {
+ return object.mOpacity;
+ }
+ };
+}
diff --git a/android/graphics/drawable/RotateDrawable.java b/android/graphics/drawable/RotateDrawable.java
new file mode 100644
index 0000000..c0dfe77
--- /dev/null
+++ b/android/graphics/drawable/RotateDrawable.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2007 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 android.graphics.drawable;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.Resources.Theme;
+import android.util.MathUtils;
+import android.util.TypedValue;
+import android.util.AttributeSet;
+
+import java.io.IOException;
+
+/**
+ * <p>
+ * A Drawable that can rotate another Drawable based on the current level value.
+ * The start and end angles of rotation can be controlled to map any circular
+ * arc to the level values range.
+ * <p>
+ * It can be defined in an XML file with the <code><rotate></code> element.
+ * For more information, see the guide to
+ * <a href="{@docRoot}guide/topics/resources/animation-resource.html">Animation Resources</a>.
+ *
+ * @attr ref android.R.styleable#RotateDrawable_visible
+ * @attr ref android.R.styleable#RotateDrawable_fromDegrees
+ * @attr ref android.R.styleable#RotateDrawable_toDegrees
+ * @attr ref android.R.styleable#RotateDrawable_pivotX
+ * @attr ref android.R.styleable#RotateDrawable_pivotY
+ * @attr ref android.R.styleable#RotateDrawable_drawable
+ */
+public class RotateDrawable extends DrawableWrapper {
+ private static final int MAX_LEVEL = 10000;
+
+ private RotateState mState;
+
+ /**
+ * Creates a new rotating drawable with no wrapped drawable.
+ */
+ public RotateDrawable() {
+ this(new RotateState(null, null), null);
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RotateDrawable);
+
+ // Inflation will advance the XmlPullParser and AttributeSet.
+ super.inflate(r, parser, attrs, theme);
+
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ a.recycle();
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ final RotateState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.RotateDrawable);
+ try {
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ } catch (XmlPullParserException e) {
+ rethrowAsRuntimeException(e);
+ } finally {
+ a.recycle();
+ }
+ }
+ }
+
+ private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
+ // If we're not waiting on a theme, verify required attributes.
+ if (getDrawable() == null && (mState.mThemeAttrs == null
+ || mState.mThemeAttrs[R.styleable.RotateDrawable_drawable] == 0)) {
+ throw new XmlPullParserException(a.getPositionDescription()
+ + ": <rotate> tag requires a 'drawable' attribute or "
+ + "child tag defining a drawable");
+ }
+ }
+
+ private void updateStateFromTypedArray(@NonNull TypedArray a) {
+ final RotateState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ if (a.hasValue(R.styleable.RotateDrawable_pivotX)) {
+ final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotX);
+ state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION;
+ state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
+ }
+
+ if (a.hasValue(R.styleable.RotateDrawable_pivotY)) {
+ final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotY);
+ state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION;
+ state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
+ }
+
+ state.mFromDegrees = a.getFloat(
+ R.styleable.RotateDrawable_fromDegrees, state.mFromDegrees);
+ state.mToDegrees = a.getFloat(
+ R.styleable.RotateDrawable_toDegrees, state.mToDegrees);
+ state.mCurrentDegrees = state.mFromDegrees;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Drawable d = getDrawable();
+ final Rect bounds = d.getBounds();
+ final int w = bounds.right - bounds.left;
+ final int h = bounds.bottom - bounds.top;
+ final RotateState st = mState;
+ final float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX;
+ final float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY;
+
+ final int saveCount = canvas.save();
+ canvas.rotate(st.mCurrentDegrees, px + bounds.left, py + bounds.top);
+ d.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+
+ /**
+ * Sets the start angle for rotation.
+ *
+ * @param fromDegrees starting angle in degrees
+ * @see #getFromDegrees()
+ * @attr ref android.R.styleable#RotateDrawable_fromDegrees
+ */
+ public void setFromDegrees(float fromDegrees) {
+ if (mState.mFromDegrees != fromDegrees) {
+ mState.mFromDegrees = fromDegrees;
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * @return starting angle for rotation in degrees
+ * @see #setFromDegrees(float)
+ * @attr ref android.R.styleable#RotateDrawable_fromDegrees
+ */
+ public float getFromDegrees() {
+ return mState.mFromDegrees;
+ }
+
+ /**
+ * Sets the end angle for rotation.
+ *
+ * @param toDegrees ending angle in degrees
+ * @see #getToDegrees()
+ * @attr ref android.R.styleable#RotateDrawable_toDegrees
+ */
+ public void setToDegrees(float toDegrees) {
+ if (mState.mToDegrees != toDegrees) {
+ mState.mToDegrees = toDegrees;
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * @return ending angle for rotation in degrees
+ * @see #setToDegrees(float)
+ * @attr ref android.R.styleable#RotateDrawable_toDegrees
+ */
+ public float getToDegrees() {
+ return mState.mToDegrees;
+ }
+
+ /**
+ * Sets the X position around which the drawable is rotated.
+ * <p>
+ * If the X pivot is relative (as specified by
+ * {@link #setPivotXRelative(boolean)}), then the position represents a
+ * fraction of the drawable width. Otherwise, the position represents an
+ * absolute value in pixels.
+ *
+ * @param pivotX X position around which to rotate
+ * @see #setPivotXRelative(boolean)
+ * @attr ref android.R.styleable#RotateDrawable_pivotX
+ */
+ public void setPivotX(float pivotX) {
+ if (mState.mPivotX != pivotX) {
+ mState.mPivotX = pivotX;
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * @return X position around which to rotate
+ * @see #setPivotX(float)
+ * @attr ref android.R.styleable#RotateDrawable_pivotX
+ */
+ public float getPivotX() {
+ return mState.mPivotX;
+ }
+
+ /**
+ * Sets whether the X pivot value represents a fraction of the drawable
+ * width or an absolute value in pixels.
+ *
+ * @param relative true if the X pivot represents a fraction of the drawable
+ * width, or false if it represents an absolute value in pixels
+ * @see #isPivotXRelative()
+ */
+ public void setPivotXRelative(boolean relative) {
+ if (mState.mPivotXRel != relative) {
+ mState.mPivotXRel = relative;
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * @return true if the X pivot represents a fraction of the drawable width,
+ * or false if it represents an absolute value in pixels
+ * @see #setPivotXRelative(boolean)
+ */
+ public boolean isPivotXRelative() {
+ return mState.mPivotXRel;
+ }
+
+ /**
+ * Sets the Y position around which the drawable is rotated.
+ * <p>
+ * If the Y pivot is relative (as specified by
+ * {@link #setPivotYRelative(boolean)}), then the position represents a
+ * fraction of the drawable height. Otherwise, the position represents an
+ * absolute value in pixels.
+ *
+ * @param pivotY Y position around which to rotate
+ * @see #getPivotY()
+ * @attr ref android.R.styleable#RotateDrawable_pivotY
+ */
+ public void setPivotY(float pivotY) {
+ if (mState.mPivotY != pivotY) {
+ mState.mPivotY = pivotY;
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * @return Y position around which to rotate
+ * @see #setPivotY(float)
+ * @attr ref android.R.styleable#RotateDrawable_pivotY
+ */
+ public float getPivotY() {
+ return mState.mPivotY;
+ }
+
+ /**
+ * Sets whether the Y pivot value represents a fraction of the drawable
+ * height or an absolute value in pixels.
+ *
+ * @param relative True if the Y pivot represents a fraction of the drawable
+ * height, or false if it represents an absolute value in pixels
+ * @see #isPivotYRelative()
+ */
+ public void setPivotYRelative(boolean relative) {
+ if (mState.mPivotYRel != relative) {
+ mState.mPivotYRel = relative;
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * @return true if the Y pivot represents a fraction of the drawable height,
+ * or false if it represents an absolute value in pixels
+ * @see #setPivotYRelative(boolean)
+ */
+ public boolean isPivotYRelative() {
+ return mState.mPivotYRel;
+ }
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ super.onLevelChange(level);
+
+ final float value = level / (float) MAX_LEVEL;
+ final float degrees = MathUtils.lerp(mState.mFromDegrees, mState.mToDegrees, value);
+ mState.mCurrentDegrees = degrees;
+
+ invalidateSelf();
+ return true;
+ }
+
+ @Override
+ DrawableWrapperState mutateConstantState() {
+ mState = new RotateState(mState, null);
+ return mState;
+ }
+
+ static final class RotateState extends DrawableWrapper.DrawableWrapperState {
+ private int[] mThemeAttrs;
+
+ boolean mPivotXRel = true;
+ float mPivotX = 0.5f;
+ boolean mPivotYRel = true;
+ float mPivotY = 0.5f;
+ float mFromDegrees = 0.0f;
+ float mToDegrees = 360.0f;
+ float mCurrentDegrees = 0.0f;
+
+ RotateState(RotateState orig, Resources res) {
+ super(orig, res);
+
+ if (orig != null) {
+ mPivotXRel = orig.mPivotXRel;
+ mPivotX = orig.mPivotX;
+ mPivotYRel = orig.mPivotYRel;
+ mPivotY = orig.mPivotY;
+ mFromDegrees = orig.mFromDegrees;
+ mToDegrees = orig.mToDegrees;
+ mCurrentDegrees = orig.mCurrentDegrees;
+ }
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new RotateDrawable(this, res);
+ }
+ }
+
+ private RotateDrawable(RotateState state, Resources res) {
+ super(state, res);
+
+ mState = state;
+ }
+}
diff --git a/android/graphics/drawable/ScaleDrawable.java b/android/graphics/drawable/ScaleDrawable.java
new file mode 100644
index 0000000..51e143b
--- /dev/null
+++ b/android/graphics/drawable/ScaleDrawable.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+
+import java.io.IOException;
+
+/**
+ * A Drawable that changes the size of another Drawable based on its current
+ * level value. You can control how much the child Drawable changes in width
+ * and height based on the level, as well as a gravity to control where it is
+ * placed in its overall container. Most often used to implement things like
+ * progress bars.
+ * <p>
+ * The default level may be specified from XML using the
+ * {@link android.R.styleable#ScaleDrawable_level android:level} property. When
+ * this property is not specified, the default level is 0, which corresponds to
+ * zero height and/or width depending on the values specified for
+ * {@code android.R.styleable#ScaleDrawable_scaleWidth scaleWidth} and
+ * {@code android.R.styleable#ScaleDrawable_scaleHeight scaleHeight}. At run
+ * time, the level may be set via {@link #setLevel(int)}.
+ * <p>
+ * A scale drawable may be defined in an XML file with the {@code <scale>}
+ * element. For more information, see the guide to
+ * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable
+ * Resources</a>.
+ *
+ * @attr ref android.R.styleable#ScaleDrawable_scaleWidth
+ * @attr ref android.R.styleable#ScaleDrawable_scaleHeight
+ * @attr ref android.R.styleable#ScaleDrawable_scaleGravity
+ * @attr ref android.R.styleable#ScaleDrawable_drawable
+ * @attr ref android.R.styleable#ScaleDrawable_level
+ */
+public class ScaleDrawable extends DrawableWrapper {
+ private static final int MAX_LEVEL = 10000;
+
+ private final Rect mTmpRect = new Rect();
+
+ private ScaleState mState;
+
+ ScaleDrawable() {
+ this(new ScaleState(null, null), null);
+ }
+
+ /**
+ * Creates a new scale drawable with the specified gravity and scale
+ * properties.
+ *
+ * @param drawable the drawable to scale
+ * @param gravity gravity constant (see {@link Gravity} used to position
+ * the scaled drawable within the parent container
+ * @param scaleWidth width scaling factor [0...1] to use then the level is
+ * at the maximum value, or -1 to not scale width
+ * @param scaleHeight height scaling factor [0...1] to use then the level
+ * is at the maximum value, or -1 to not scale height
+ */
+ public ScaleDrawable(Drawable drawable, int gravity, float scaleWidth, float scaleHeight) {
+ this(new ScaleState(null, null), null);
+
+ mState.mGravity = gravity;
+ mState.mScaleWidth = scaleWidth;
+ mState.mScaleHeight = scaleHeight;
+
+ setDrawable(drawable);
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ScaleDrawable);
+
+ // Inflation will advance the XmlPullParser and AttributeSet.
+ super.inflate(r, parser, attrs, theme);
+
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ a.recycle();
+
+ updateLocalState();
+ }
+
+ @Override
+ public void applyTheme(@NonNull Theme t) {
+ super.applyTheme(t);
+
+ final ScaleState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.ScaleDrawable);
+ try {
+ updateStateFromTypedArray(a);
+ verifyRequiredAttributes(a);
+ } catch (XmlPullParserException e) {
+ rethrowAsRuntimeException(e);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ updateLocalState();
+ }
+
+ private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
+ // If we're not waiting on a theme, verify required attributes.
+ if (getDrawable() == null && (mState.mThemeAttrs == null
+ || mState.mThemeAttrs[R.styleable.ScaleDrawable_drawable] == 0)) {
+ throw new XmlPullParserException(a.getPositionDescription()
+ + ": <scale> tag requires a 'drawable' attribute or "
+ + "child tag defining a drawable");
+ }
+ }
+
+ private void updateStateFromTypedArray(@NonNull TypedArray a) {
+ final ScaleState state = mState;
+ if (state == null) {
+ return;
+ }
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ state.mScaleWidth = getPercent(a,
+ R.styleable.ScaleDrawable_scaleWidth, state.mScaleWidth);
+ state.mScaleHeight = getPercent(a,
+ R.styleable.ScaleDrawable_scaleHeight, state.mScaleHeight);
+ state.mGravity = a.getInt(
+ R.styleable.ScaleDrawable_scaleGravity, state.mGravity);
+ state.mUseIntrinsicSizeAsMin = a.getBoolean(
+ R.styleable.ScaleDrawable_useIntrinsicSizeAsMinimum, state.mUseIntrinsicSizeAsMin);
+ state.mInitialLevel = a.getInt(
+ R.styleable.ScaleDrawable_level, state.mInitialLevel);
+ }
+
+ private static float getPercent(TypedArray a, int index, float defaultValue) {
+ final int type = a.getType(index);
+ if (type == TypedValue.TYPE_FRACTION || type == TypedValue.TYPE_NULL) {
+ return a.getFraction(index, 1, 1, defaultValue);
+ }
+
+ // Coerce to float.
+ final String s = a.getString(index);
+ if (s != null) {
+ if (s.endsWith("%")) {
+ final String f = s.substring(0, s.length() - 1);
+ return Float.parseFloat(f) / 100.0f;
+ }
+ }
+
+ return defaultValue;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Drawable d = getDrawable();
+ if (d != null && d.getLevel() != 0) {
+ d.draw(canvas);
+ }
+ }
+
+ @Override
+ public int getOpacity() {
+ final Drawable d = getDrawable();
+ if (d.getLevel() == 0) {
+ return PixelFormat.TRANSPARENT;
+ }
+
+ final int opacity = d.getOpacity();
+ if (opacity == PixelFormat.OPAQUE && d.getLevel() < MAX_LEVEL) {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ return opacity;
+ }
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ super.onLevelChange(level);
+ onBoundsChange(getBounds());
+ invalidateSelf();
+ return true;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ final Drawable d = getDrawable();
+ final Rect r = mTmpRect;
+ final boolean min = mState.mUseIntrinsicSizeAsMin;
+ final int level = getLevel();
+
+ int w = bounds.width();
+ if (mState.mScaleWidth > 0) {
+ final int iw = min ? d.getIntrinsicWidth() : 0;
+ w -= (int) ((w - iw) * (MAX_LEVEL - level) * mState.mScaleWidth / MAX_LEVEL);
+ }
+
+ int h = bounds.height();
+ if (mState.mScaleHeight > 0) {
+ final int ih = min ? d.getIntrinsicHeight() : 0;
+ h -= (int) ((h - ih) * (MAX_LEVEL - level) * mState.mScaleHeight / MAX_LEVEL);
+ }
+
+ final int layoutDirection = getLayoutDirection();
+ Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection);
+
+ if (w > 0 && h > 0) {
+ d.setBounds(r.left, r.top, r.right, r.bottom);
+ }
+ }
+
+ @Override
+ DrawableWrapperState mutateConstantState() {
+ mState = new ScaleState(mState, null);
+ return mState;
+ }
+
+ static final class ScaleState extends DrawableWrapper.DrawableWrapperState {
+ /** Constant used to disable scaling for a particular dimension. */
+ private static final float DO_NOT_SCALE = -1.0f;
+
+ private int[] mThemeAttrs;
+
+ float mScaleWidth = DO_NOT_SCALE;
+ float mScaleHeight = DO_NOT_SCALE;
+ int mGravity = Gravity.LEFT;
+ boolean mUseIntrinsicSizeAsMin = false;
+ int mInitialLevel = 0;
+
+ ScaleState(ScaleState orig, Resources res) {
+ super(orig, res);
+
+ if (orig != null) {
+ mScaleWidth = orig.mScaleWidth;
+ mScaleHeight = orig.mScaleHeight;
+ mGravity = orig.mGravity;
+ mUseIntrinsicSizeAsMin = orig.mUseIntrinsicSizeAsMin;
+ mInitialLevel = orig.mInitialLevel;
+ }
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new ScaleDrawable(this, res);
+ }
+ }
+
+ /**
+ * Creates a new ScaleDrawable based on the specified constant state.
+ * <p>
+ * The resulting drawable is guaranteed to have a new constant state.
+ *
+ * @param state constant state from which the drawable inherits
+ */
+ private ScaleDrawable(ScaleState state, Resources res) {
+ super(state, res);
+
+ mState = state;
+
+ updateLocalState();
+ }
+
+ private void updateLocalState() {
+ setLevel(mState.mInitialLevel);
+ }
+}
+
diff --git a/android/graphics/drawable/ShapeDrawable.java b/android/graphics/drawable/ShapeDrawable.java
new file mode 100644
index 0000000..34da928
--- /dev/null
+++ b/android/graphics/drawable/ShapeDrawable.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (C) 2007 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 android.graphics.drawable;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.Xfermode;
+import android.graphics.drawable.shapes.Shape;
+import android.util.AttributeSet;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * A Drawable object that draws primitive shapes. A ShapeDrawable takes a
+ * {@link android.graphics.drawable.shapes.Shape} object and manages its
+ * presence on the screen. If no Shape is given, then the ShapeDrawable will
+ * default to a {@link android.graphics.drawable.shapes.RectShape}.
+ * <p>
+ * This object can be defined in an XML file with the <code><shape></code>
+ * element.
+ * </p>
+ * <div class="special reference"> <h3>Developer Guides</h3>
+ * <p>
+ * For more information about how to use ShapeDrawable, read the <a
+ * href="{@docRoot}guide/topics/graphics/2d-graphics.html#shape-drawable">
+ * Canvas and Drawables</a> document. For more information about defining a
+ * ShapeDrawable in XML, read the
+ * <a href="{@docRoot}guide/topics/resources/drawable-resource.html#Shape">
+ * Drawable Resources</a> document.
+ * </p>
+ * </div>
+ *
+ * @attr ref android.R.styleable#ShapeDrawablePadding_left
+ * @attr ref android.R.styleable#ShapeDrawablePadding_top
+ * @attr ref android.R.styleable#ShapeDrawablePadding_right
+ * @attr ref android.R.styleable#ShapeDrawablePadding_bottom
+ * @attr ref android.R.styleable#ShapeDrawable_color
+ * @attr ref android.R.styleable#ShapeDrawable_width
+ * @attr ref android.R.styleable#ShapeDrawable_height
+ */
+public class ShapeDrawable extends Drawable {
+ private @NonNull ShapeState mShapeState;
+ private PorterDuffColorFilter mTintFilter;
+ private boolean mMutated;
+
+ /**
+ * ShapeDrawable constructor.
+ */
+ public ShapeDrawable() {
+ this(new ShapeState(), null);
+ }
+
+ /**
+ * Creates a ShapeDrawable with a specified Shape.
+ *
+ * @param s the Shape that this ShapeDrawable should be
+ */
+ public ShapeDrawable(Shape s) {
+ this(new ShapeState(), null);
+
+ mShapeState.mShape = s;
+ }
+
+ /**
+ * Returns the Shape of this ShapeDrawable.
+ */
+ public Shape getShape() {
+ return mShapeState.mShape;
+ }
+
+ /**
+ * Sets the Shape of this ShapeDrawable.
+ */
+ public void setShape(Shape s) {
+ mShapeState.mShape = s;
+ updateShape();
+ }
+
+ /**
+ * Sets a ShaderFactory to which requests for a
+ * {@link android.graphics.Shader} object will be made.
+ *
+ * @param fact an instance of your ShaderFactory implementation
+ */
+ public void setShaderFactory(ShaderFactory fact) {
+ mShapeState.mShaderFactory = fact;
+ }
+
+ /**
+ * Returns the ShaderFactory used by this ShapeDrawable for requesting a
+ * {@link android.graphics.Shader}.
+ */
+ public ShaderFactory getShaderFactory() {
+ return mShapeState.mShaderFactory;
+ }
+
+ /**
+ * Returns the Paint used to draw the shape.
+ */
+ public Paint getPaint() {
+ return mShapeState.mPaint;
+ }
+
+ /**
+ * Sets padding for the shape.
+ *
+ * @param left padding for the left side (in pixels)
+ * @param top padding for the top (in pixels)
+ * @param right padding for the right side (in pixels)
+ * @param bottom padding for the bottom (in pixels)
+ */
+ public void setPadding(int left, int top, int right, int bottom) {
+ if ((left | top | right | bottom) == 0) {
+ mShapeState.mPadding = null;
+ } else {
+ if (mShapeState.mPadding == null) {
+ mShapeState.mPadding = new Rect();
+ }
+ mShapeState.mPadding.set(left, top, right, bottom);
+ }
+ invalidateSelf();
+ }
+
+ /**
+ * Sets padding for this shape, defined by a Rect object. Define the padding
+ * in the Rect object as: left, top, right, bottom.
+ */
+ public void setPadding(Rect padding) {
+ if (padding == null) {
+ mShapeState.mPadding = null;
+ } else {
+ if (mShapeState.mPadding == null) {
+ mShapeState.mPadding = new Rect();
+ }
+ mShapeState.mPadding.set(padding);
+ }
+ invalidateSelf();
+ }
+
+ /**
+ * Sets the intrinsic (default) width for this shape.
+ *
+ * @param width the intrinsic width (in pixels)
+ */
+ public void setIntrinsicWidth(int width) {
+ mShapeState.mIntrinsicWidth = width;
+ invalidateSelf();
+ }
+
+ /**
+ * Sets the intrinsic (default) height for this shape.
+ *
+ * @param height the intrinsic height (in pixels)
+ */
+ public void setIntrinsicHeight(int height) {
+ mShapeState.mIntrinsicHeight = height;
+ invalidateSelf();
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mShapeState.mIntrinsicWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mShapeState.mIntrinsicHeight;
+ }
+
+ @Override
+ public boolean getPadding(Rect padding) {
+ if (mShapeState.mPadding != null) {
+ padding.set(mShapeState.mPadding);
+ return true;
+ } else {
+ return super.getPadding(padding);
+ }
+ }
+
+ private static int modulateAlpha(int paintAlpha, int alpha) {
+ int scale = alpha + (alpha >>> 7); // convert to 0..256
+ return paintAlpha * scale >>> 8;
+ }
+
+ /**
+ * Called from the drawable's draw() method after the canvas has been set to
+ * draw the shape at (0,0). Subclasses can override for special effects such
+ * as multiple layers, stroking, etc.
+ */
+ protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
+ shape.draw(canvas, paint);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Rect r = getBounds();
+ final ShapeState state = mShapeState;
+ final Paint paint = state.mPaint;
+
+ final int prevAlpha = paint.getAlpha();
+ paint.setAlpha(modulateAlpha(prevAlpha, state.mAlpha));
+
+ // only draw shape if it may affect output
+ if (paint.getAlpha() != 0 || paint.getXfermode() != null || paint.hasShadowLayer()) {
+ final boolean clearColorFilter;
+ if (mTintFilter != null && paint.getColorFilter() == null) {
+ paint.setColorFilter(mTintFilter);
+ clearColorFilter = true;
+ } else {
+ clearColorFilter = false;
+ }
+
+ if (state.mShape != null) {
+ // need the save both for the translate, and for the (unknown)
+ // Shape
+ final int count = canvas.save();
+ canvas.translate(r.left, r.top);
+ onDraw(state.mShape, canvas, paint);
+ canvas.restoreToCount(count);
+ } else {
+ canvas.drawRect(r, paint);
+ }
+
+ if (clearColorFilter) {
+ paint.setColorFilter(null);
+ }
+ }
+
+ // restore
+ paint.setAlpha(prevAlpha);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations() | mShapeState.getChangingConfigurations();
+ }
+
+ /**
+ * Set the alpha level for this drawable [0..255]. Note that this drawable
+ * also has a color in its paint, which has an alpha as well. These two
+ * values are automatically combined during drawing. Thus if the color's
+ * alpha is 75% (i.e. 192) and the drawable's alpha is 50% (i.e. 128), then
+ * the combined alpha that will be used during drawing will be 37.5% (i.e.
+ * 96).
+ */
+ @Override
+ public void setAlpha(int alpha) {
+ mShapeState.mAlpha = alpha;
+ invalidateSelf();
+ }
+
+ @Override
+ public int getAlpha() {
+ return mShapeState.mAlpha;
+ }
+
+ @Override
+ public void setTintList(ColorStateList tint) {
+ mShapeState.mTint = tint;
+ mTintFilter = updateTintFilter(mTintFilter, tint, mShapeState.mTintMode);
+ invalidateSelf();
+ }
+
+ @Override
+ public void setTintMode(PorterDuff.Mode tintMode) {
+ mShapeState.mTintMode = tintMode;
+ mTintFilter = updateTintFilter(mTintFilter, mShapeState.mTint, tintMode);
+ invalidateSelf();
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mShapeState.mPaint.setColorFilter(colorFilter);
+ invalidateSelf();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @TestApi
+ public void setXfermode(@Nullable Xfermode mode) {
+ mShapeState.mPaint.setXfermode(mode);
+ invalidateSelf();
+ }
+
+ @Override
+ public int getOpacity() {
+ if (mShapeState.mShape == null) {
+ final Paint p = mShapeState.mPaint;
+ if (p.getXfermode() == null) {
+ final int alpha = p.getAlpha();
+ if (alpha == 0) {
+ return PixelFormat.TRANSPARENT;
+ }
+ if (alpha == 255) {
+ return PixelFormat.OPAQUE;
+ }
+ }
+ }
+ // not sure, so be safe
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setDither(boolean dither) {
+ mShapeState.mPaint.setDither(dither);
+ invalidateSelf();
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ updateShape();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] stateSet) {
+ final ShapeState state = mShapeState;
+ if (state.mTint != null && state.mTintMode != null) {
+ mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isStateful() {
+ final ShapeState s = mShapeState;
+ return super.isStateful() || (s.mTint != null && s.mTint.isStateful());
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return mShapeState.mTint != null && mShapeState.mTint.hasFocusStateSpecified();
+ }
+
+ /**
+ * Subclasses override this to parse custom subelements. If you handle it,
+ * return true, else return <em>super.inflateTag(...)</em>.
+ */
+ protected boolean inflateTag(String name, Resources r, XmlPullParser parser,
+ AttributeSet attrs) {
+
+ if ("padding".equals(name)) {
+ TypedArray a = r.obtainAttributes(attrs,
+ com.android.internal.R.styleable.ShapeDrawablePadding);
+ setPadding(
+ a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.ShapeDrawablePadding_left, 0),
+ a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.ShapeDrawablePadding_top, 0),
+ a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.ShapeDrawablePadding_right, 0),
+ a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.ShapeDrawablePadding_bottom, 0));
+ a.recycle();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
+ throws XmlPullParserException, IOException {
+ super.inflate(r, parser, attrs, theme);
+
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ShapeDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+
+ int type;
+ final int outerDepth = parser.getDepth();
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ final String name = parser.getName();
+ // call our subclass
+ if (!inflateTag(name, r, parser, attrs)) {
+ android.util.Log.w("drawable", "Unknown element: " + name +
+ " for ShapeDrawable " + this);
+ }
+ }
+
+ // Update local properties.
+ updateLocalState();
+ }
+
+ @Override
+ public void applyTheme(Theme t) {
+ super.applyTheme(t);
+
+ final ShapeState state = mShapeState;
+ if (state == null) {
+ return;
+ }
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.ShapeDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+ }
+
+ // Apply theme to contained color state list.
+ if (state.mTint != null && state.mTint.canApplyTheme()) {
+ state.mTint = state.mTint.obtainForTheme(t);
+ }
+
+ // Update local properties.
+ updateLocalState();
+ }
+
+ private void updateStateFromTypedArray(TypedArray a) {
+ final ShapeState state = mShapeState;
+ final Paint paint = state.mPaint;
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ int color = paint.getColor();
+ color = a.getColor(R.styleable.ShapeDrawable_color, color);
+ paint.setColor(color);
+
+ boolean dither = paint.isDither();
+ dither = a.getBoolean(R.styleable.ShapeDrawable_dither, dither);
+ paint.setDither(dither);
+
+ state.mIntrinsicWidth = (int) a.getDimension(
+ R.styleable.ShapeDrawable_width, state.mIntrinsicWidth);
+ state.mIntrinsicHeight = (int) a.getDimension(
+ R.styleable.ShapeDrawable_height, state.mIntrinsicHeight);
+
+ final int tintMode = a.getInt(R.styleable.ShapeDrawable_tintMode, -1);
+ if (tintMode != -1) {
+ state.mTintMode = Drawable.parseTintMode(tintMode, Mode.SRC_IN);
+ }
+
+ final ColorStateList tint = a.getColorStateList(R.styleable.ShapeDrawable_tint);
+ if (tint != null) {
+ state.mTint = tint;
+ }
+ }
+
+ private void updateShape() {
+ if (mShapeState.mShape != null) {
+ final Rect r = getBounds();
+ final int w = r.width();
+ final int h = r.height();
+
+ mShapeState.mShape.resize(w, h);
+ if (mShapeState.mShaderFactory != null) {
+ mShapeState.mPaint.setShader(mShapeState.mShaderFactory.resize(w, h));
+ }
+ }
+ invalidateSelf();
+ }
+
+ @Override
+ public void getOutline(Outline outline) {
+ if (mShapeState.mShape != null) {
+ mShapeState.mShape.getOutline(outline);
+ outline.setAlpha(getAlpha() / 255.0f);
+ }
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ mShapeState.mChangingConfigurations = getChangingConfigurations();
+ return mShapeState;
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mShapeState = new ShapeState(mShapeState);
+ updateLocalState();
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mMutated = false;
+ }
+
+ /**
+ * Defines the intrinsic properties of this ShapeDrawable's Shape.
+ */
+ static final class ShapeState extends ConstantState {
+ final @NonNull Paint mPaint;
+
+ @Config int mChangingConfigurations;
+ int[] mThemeAttrs;
+ Shape mShape;
+ ColorStateList mTint;
+ Mode mTintMode = DEFAULT_TINT_MODE;
+ Rect mPadding;
+ int mIntrinsicWidth;
+ int mIntrinsicHeight;
+ int mAlpha = 255;
+ ShaderFactory mShaderFactory;
+
+ /**
+ * Constructs a new ShapeState.
+ */
+ ShapeState() {
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ }
+
+ /**
+ * Constructs a new ShapeState that contains a deep copy of the
+ * specified ShapeState.
+ *
+ * @param orig the state to create a deep copy of
+ */
+ ShapeState(@NonNull ShapeState orig) {
+ mChangingConfigurations = orig.mChangingConfigurations;
+ mPaint = new Paint(orig.mPaint);
+ mThemeAttrs = orig.mThemeAttrs;
+ if (orig.mShape != null) {
+ try {
+ mShape = orig.mShape.clone();
+ } catch (CloneNotSupportedException e) {
+ // Well, at least we tried.
+ mShape = orig.mShape;
+ }
+ }
+ mTint = orig.mTint;
+ mTintMode = orig.mTintMode;
+ if (orig.mPadding != null) {
+ mPadding = new Rect(orig.mPadding);
+ }
+ mIntrinsicWidth = orig.mIntrinsicWidth;
+ mIntrinsicHeight = orig.mIntrinsicHeight;
+ mAlpha = orig.mAlpha;
+
+ // We don't have any way to clone a shader factory, so hopefully
+ // this class doesn't contain any local state.
+ mShaderFactory = orig.mShaderFactory;
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mThemeAttrs != null
+ || (mTint != null && mTint.canApplyTheme());
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new ShapeDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new ShapeDrawable(this, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations
+ | (mTint != null ? mTint.getChangingConfigurations() : 0);
+ }
+ }
+
+ /**
+ * The one constructor to rule them all. This is called by all public
+ * constructors to set the state and initialize local properties.
+ */
+ private ShapeDrawable(ShapeState state, Resources res) {
+ mShapeState = state;
+
+ updateLocalState();
+ }
+
+ /**
+ * Initializes local dynamic properties from state. This should be called
+ * after significant state changes, e.g. from the One True Constructor and
+ * after inflating or applying a theme.
+ */
+ private void updateLocalState() {
+ mTintFilter = updateTintFilter(mTintFilter, mShapeState.mTint, mShapeState.mTintMode);
+ }
+
+ /**
+ * Base class defines a factory object that is called each time the drawable
+ * is resized (has a new width or height). Its resize() method returns a
+ * corresponding shader, or null. Implement this class if you'd like your
+ * ShapeDrawable to use a special {@link android.graphics.Shader}, such as a
+ * {@link android.graphics.LinearGradient}.
+ */
+ public static abstract class ShaderFactory {
+ /**
+ * Returns the Shader to be drawn when a Drawable is drawn. The
+ * dimensions of the Drawable are passed because they may be needed to
+ * adjust how the Shader is configured for drawing. This is called by
+ * ShapeDrawable.setShape().
+ *
+ * @param width the width of the Drawable being drawn
+ * @param height the heigh of the Drawable being drawn
+ * @return the Shader to be drawn
+ */
+ public abstract Shader resize(int width, int height);
+ }
+}
diff --git a/android/graphics/drawable/StateListDrawable.java b/android/graphics/drawable/StateListDrawable.java
new file mode 100644
index 0000000..c98f160
--- /dev/null
+++ b/android/graphics/drawable/StateListDrawable.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2006 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 android.graphics.drawable;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.Resources.Theme;
+import android.util.AttributeSet;
+import android.util.StateSet;
+
+/**
+ * Lets you assign a number of graphic images to a single Drawable and swap out the visible item by a string
+ * ID value.
+ * <p/>
+ * <p>It can be defined in an XML file with the <code><selector></code> element.
+ * Each state Drawable is defined in a nested <code><item></code> element. For more
+ * information, see the guide to <a
+ * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
+ *
+ * @attr ref android.R.styleable#StateListDrawable_visible
+ * @attr ref android.R.styleable#StateListDrawable_variablePadding
+ * @attr ref android.R.styleable#StateListDrawable_constantSize
+ * @attr ref android.R.styleable#DrawableStates_state_focused
+ * @attr ref android.R.styleable#DrawableStates_state_window_focused
+ * @attr ref android.R.styleable#DrawableStates_state_enabled
+ * @attr ref android.R.styleable#DrawableStates_state_checkable
+ * @attr ref android.R.styleable#DrawableStates_state_checked
+ * @attr ref android.R.styleable#DrawableStates_state_selected
+ * @attr ref android.R.styleable#DrawableStates_state_activated
+ * @attr ref android.R.styleable#DrawableStates_state_active
+ * @attr ref android.R.styleable#DrawableStates_state_single
+ * @attr ref android.R.styleable#DrawableStates_state_first
+ * @attr ref android.R.styleable#DrawableStates_state_middle
+ * @attr ref android.R.styleable#DrawableStates_state_last
+ * @attr ref android.R.styleable#DrawableStates_state_pressed
+ */
+public class StateListDrawable extends DrawableContainer {
+ private static final String TAG = "StateListDrawable";
+
+ private static final boolean DEBUG = false;
+
+ private StateListState mStateListState;
+ private boolean mMutated;
+
+ public StateListDrawable() {
+ this(null, null);
+ }
+
+ /**
+ * Add a new image/string ID to the set of images.
+ *
+ * @param stateSet - An array of resource Ids to associate with the image.
+ * Switch to this image by calling setState().
+ * @param drawable -The image to show.
+ */
+ public void addState(int[] stateSet, Drawable drawable) {
+ if (drawable != null) {
+ mStateListState.addStateSet(stateSet, drawable);
+ // in case the new state matches our current state...
+ onStateChange(getState());
+ }
+ }
+
+ @Override
+ public boolean isStateful() {
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return mStateListState.hasFocusStateSpecified();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] stateSet) {
+ final boolean changed = super.onStateChange(stateSet);
+
+ int idx = mStateListState.indexOfStateSet(stateSet);
+ if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "
+ + Arrays.toString(stateSet) + " found " + idx);
+ if (idx < 0) {
+ idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
+ }
+
+ return selectDrawable(idx) || changed;
+ }
+
+ @Override
+ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.StateListDrawable);
+ super.inflateWithAttributes(r, parser, a, R.styleable.StateListDrawable_visible);
+ updateStateFromTypedArray(a);
+ updateDensity(r);
+ a.recycle();
+
+ inflateChildElements(r, parser, attrs, theme);
+
+ onStateChange(getState());
+ }
+
+ /**
+ * Updates the constant state from the values in the typed array.
+ */
+ private void updateStateFromTypedArray(TypedArray a) {
+ final StateListState state = mStateListState;
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ state.mVariablePadding = a.getBoolean(
+ R.styleable.StateListDrawable_variablePadding, state.mVariablePadding);
+ state.mConstantSize = a.getBoolean(
+ R.styleable.StateListDrawable_constantSize, state.mConstantSize);
+ state.mEnterFadeDuration = a.getInt(
+ R.styleable.StateListDrawable_enterFadeDuration, state.mEnterFadeDuration);
+ state.mExitFadeDuration = a.getInt(
+ R.styleable.StateListDrawable_exitFadeDuration, state.mExitFadeDuration);
+ state.mDither = a.getBoolean(
+ R.styleable.StateListDrawable_dither, state.mDither);
+ state.mAutoMirrored = a.getBoolean(
+ R.styleable.StateListDrawable_autoMirrored, state.mAutoMirrored);
+ }
+
+ /**
+ * Inflates child elements from XML.
+ */
+ private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
+ Theme theme) throws XmlPullParserException, IOException {
+ final StateListState state = mStateListState;
+ final int innerDepth = parser.getDepth() + 1;
+ int type;
+ int depth;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth
+ || type != XmlPullParser.END_TAG)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (depth > innerDepth || !parser.getName().equals("item")) {
+ continue;
+ }
+
+ // This allows state list drawable item elements to be themed at
+ // inflation time but does NOT make them work for Zygote preload.
+ final TypedArray a = obtainAttributes(r, theme, attrs,
+ R.styleable.StateListDrawableItem);
+ Drawable dr = a.getDrawable(R.styleable.StateListDrawableItem_drawable);
+ a.recycle();
+
+ final int[] states = extractStateSet(attrs);
+
+ // Loading child elements modifies the state of the AttributeSet's
+ // underlying parser, so it needs to happen after obtaining
+ // attributes and extracting states.
+ if (dr == null) {
+ while ((type = parser.next()) == XmlPullParser.TEXT) {
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException(
+ parser.getPositionDescription()
+ + ": <item> tag requires a 'drawable' attribute or "
+ + "child tag defining a drawable");
+ }
+ dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
+ }
+
+ state.addStateSet(states, dr);
+ }
+ }
+
+ /**
+ * Extracts state_ attributes from an attribute set.
+ *
+ * @param attrs The attribute set.
+ * @return An array of state_ attributes.
+ */
+ int[] extractStateSet(AttributeSet attrs) {
+ int j = 0;
+ final int numAttrs = attrs.getAttributeCount();
+ int[] states = new int[numAttrs];
+ for (int i = 0; i < numAttrs; i++) {
+ final int stateResId = attrs.getAttributeNameResource(i);
+ switch (stateResId) {
+ case 0:
+ break;
+ case R.attr.drawable:
+ case R.attr.id:
+ // Ignore attributes from StateListDrawableItem and
+ // AnimatedStateListDrawableItem.
+ continue;
+ default:
+ states[j++] = attrs.getAttributeBooleanValue(i, false)
+ ? stateResId : -stateResId;
+ }
+ }
+ states = StateSet.trimStateSet(states, j);
+ return states;
+ }
+
+ StateListState getStateListState() {
+ return mStateListState;
+ }
+
+ /**
+ * Gets the number of states contained in this drawable.
+ *
+ * @return The number of states contained in this drawable.
+ * @hide pending API council
+ * @see #getStateSet(int)
+ * @see #getStateDrawable(int)
+ */
+ public int getStateCount() {
+ return mStateListState.getChildCount();
+ }
+
+ /**
+ * Gets the state set at an index.
+ *
+ * @param index The index of the state set.
+ * @return The state set at the index.
+ * @hide pending API council
+ * @see #getStateCount()
+ * @see #getStateDrawable(int)
+ */
+ public int[] getStateSet(int index) {
+ return mStateListState.mStateSets[index];
+ }
+
+ /**
+ * Gets the drawable at an index.
+ *
+ * @param index The index of the drawable.
+ * @return The drawable at the index.
+ * @hide pending API council
+ * @see #getStateCount()
+ * @see #getStateSet(int)
+ */
+ public Drawable getStateDrawable(int index) {
+ return mStateListState.getChild(index);
+ }
+
+ /**
+ * Gets the index of the drawable with the provided state set.
+ *
+ * @param stateSet the state set to look up
+ * @return the index of the provided state set, or -1 if not found
+ * @hide pending API council
+ * @see #getStateDrawable(int)
+ * @see #getStateSet(int)
+ */
+ public int getStateDrawableIndex(int[] stateSet) {
+ return mStateListState.indexOfStateSet(stateSet);
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mStateListState.mutate();
+ mMutated = true;
+ }
+ return this;
+ }
+
+ @Override
+ StateListState cloneConstantState() {
+ return new StateListState(mStateListState, this, null);
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mMutated = false;
+ }
+
+ static class StateListState extends DrawableContainerState {
+ int[] mThemeAttrs;
+ int[][] mStateSets;
+
+ StateListState(StateListState orig, StateListDrawable owner, Resources res) {
+ super(orig, owner, res);
+
+ if (orig != null) {
+ // Perform a shallow copy and rely on mutate() to deep-copy.
+ mThemeAttrs = orig.mThemeAttrs;
+ mStateSets = orig.mStateSets;
+ } else {
+ mThemeAttrs = null;
+ mStateSets = new int[getCapacity()][];
+ }
+ }
+
+ void mutate() {
+ mThemeAttrs = mThemeAttrs != null ? mThemeAttrs.clone() : null;
+
+ final int[][] stateSets = new int[mStateSets.length][];
+ for (int i = mStateSets.length - 1; i >= 0; i--) {
+ stateSets[i] = mStateSets[i] != null ? mStateSets[i].clone() : null;
+ }
+ mStateSets = stateSets;
+ }
+
+ int addStateSet(int[] stateSet, Drawable drawable) {
+ final int pos = addChild(drawable);
+ mStateSets[pos] = stateSet;
+ return pos;
+ }
+
+ int indexOfStateSet(int[] stateSet) {
+ final int[][] stateSets = mStateSets;
+ final int N = getChildCount();
+ for (int i = 0; i < N; i++) {
+ if (StateSet.stateSetMatches(stateSets[i], stateSet)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ boolean hasFocusStateSpecified() {
+ return StateSet.containsAttribute(mStateSets, R.attr.state_focused);
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new StateListDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new StateListDrawable(this, res);
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mThemeAttrs != null || super.canApplyTheme();
+ }
+
+ @Override
+ public void growArray(int oldSize, int newSize) {
+ super.growArray(oldSize, newSize);
+ final int[][] newStateSets = new int[newSize][];
+ System.arraycopy(mStateSets, 0, newStateSets, 0, oldSize);
+ mStateSets = newStateSets;
+ }
+ }
+
+ @Override
+ public void applyTheme(Theme theme) {
+ super.applyTheme(theme);
+
+ onStateChange(getState());
+ }
+
+ protected void setConstantState(@NonNull DrawableContainerState state) {
+ super.setConstantState(state);
+
+ if (state instanceof StateListState) {
+ mStateListState = (StateListState) state;
+ }
+ }
+
+ private StateListDrawable(StateListState state, Resources res) {
+ // Every state list drawable has its own constant state.
+ final StateListState newState = new StateListState(state, this, res);
+ setConstantState(newState);
+ onStateChange(getState());
+ }
+
+ /**
+ * This constructor exists so subclasses can avoid calling the default
+ * constructor and setting up a StateListDrawable-specific constant state.
+ */
+ StateListDrawable(@Nullable StateListState state) {
+ if (state != null) {
+ setConstantState(state);
+ }
+ }
+}
+
diff --git a/android/graphics/drawable/TransitionDrawable.java b/android/graphics/drawable/TransitionDrawable.java
new file mode 100644
index 0000000..3dfd680
--- /dev/null
+++ b/android/graphics/drawable/TransitionDrawable.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2008 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 android.graphics.drawable;
+
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.os.SystemClock;
+
+/**
+ * An extension of LayerDrawables that is intended to cross-fade between
+ * the first and second layer. To start the transition, call {@link #startTransition(int)}. To
+ * display just the first layer, call {@link #resetTransition()}.
+ * <p>
+ * It can be defined in an XML file with the <code><transition></code> element.
+ * Each Drawable in the transition is defined in a nested <code><item></code>. For more
+ * information, see the guide to <a
+ * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
+ *
+ * @attr ref android.R.styleable#LayerDrawableItem_left
+ * @attr ref android.R.styleable#LayerDrawableItem_top
+ * @attr ref android.R.styleable#LayerDrawableItem_right
+ * @attr ref android.R.styleable#LayerDrawableItem_bottom
+ * @attr ref android.R.styleable#LayerDrawableItem_drawable
+ * @attr ref android.R.styleable#LayerDrawableItem_id
+ *
+ */
+public class TransitionDrawable extends LayerDrawable implements Drawable.Callback {
+
+ /**
+ * A transition is about to start.
+ */
+ private static final int TRANSITION_STARTING = 0;
+
+ /**
+ * The transition has started and the animation is in progress
+ */
+ private static final int TRANSITION_RUNNING = 1;
+
+ /**
+ * No transition will be applied
+ */
+ private static final int TRANSITION_NONE = 2;
+
+ /**
+ * The current state of the transition. One of {@link #TRANSITION_STARTING},
+ * {@link #TRANSITION_RUNNING} and {@link #TRANSITION_NONE}
+ */
+ private int mTransitionState = TRANSITION_NONE;
+
+ private boolean mReverse;
+ private long mStartTimeMillis;
+ private int mFrom;
+ private int mTo;
+ private int mDuration;
+ private int mOriginalDuration;
+ private int mAlpha = 0;
+ private boolean mCrossFade;
+
+ /**
+ * Create a new transition drawable with the specified list of layers. At least
+ * 2 layers are required for this drawable to work properly.
+ */
+ public TransitionDrawable(Drawable[] layers) {
+ this(new TransitionState(null, null, null), layers);
+ }
+
+ /**
+ * Create a new transition drawable with no layer. To work correctly, at least 2
+ * layers must be added to this drawable.
+ *
+ * @see #TransitionDrawable(Drawable[])
+ */
+ TransitionDrawable() {
+ this(new TransitionState(null, null, null), (Resources) null);
+ }
+
+ private TransitionDrawable(TransitionState state, Resources res) {
+ super(state, res);
+ }
+
+ private TransitionDrawable(TransitionState state, Drawable[] layers) {
+ super(layers, state);
+ }
+
+ @Override
+ LayerState createConstantState(LayerState state, Resources res) {
+ return new TransitionState((TransitionState) state, this, res);
+ }
+
+ /**
+ * Begin the second layer on top of the first layer.
+ *
+ * @param durationMillis The length of the transition in milliseconds
+ */
+ public void startTransition(int durationMillis) {
+ mFrom = 0;
+ mTo = 255;
+ mAlpha = 0;
+ mDuration = mOriginalDuration = durationMillis;
+ mReverse = false;
+ mTransitionState = TRANSITION_STARTING;
+ invalidateSelf();
+ }
+
+ /**
+ * Show the second layer on top of the first layer immediately
+ *
+ * @hide
+ */
+ public void showSecondLayer() {
+ mAlpha = 255;
+ mReverse = false;
+ mTransitionState = TRANSITION_NONE;
+ invalidateSelf();
+ }
+
+ /**
+ * Show only the first layer.
+ */
+ public void resetTransition() {
+ mAlpha = 0;
+ mTransitionState = TRANSITION_NONE;
+ invalidateSelf();
+ }
+
+ /**
+ * Reverses the transition, picking up where the transition currently is.
+ * If the transition is not currently running, this will start the transition
+ * with the specified duration. If the transition is already running, the last
+ * known duration will be used.
+ *
+ * @param duration The duration to use if no transition is running.
+ */
+ public void reverseTransition(int duration) {
+ final long time = SystemClock.uptimeMillis();
+ // Animation is over
+ if (time - mStartTimeMillis > mDuration) {
+ if (mTo == 0) {
+ mFrom = 0;
+ mTo = 255;
+ mAlpha = 0;
+ mReverse = false;
+ } else {
+ mFrom = 255;
+ mTo = 0;
+ mAlpha = 255;
+ mReverse = true;
+ }
+ mDuration = mOriginalDuration = duration;
+ mTransitionState = TRANSITION_STARTING;
+ invalidateSelf();
+ return;
+ }
+
+ mReverse = !mReverse;
+ mFrom = mAlpha;
+ mTo = mReverse ? 0 : 255;
+ mDuration = (int) (mReverse ? time - mStartTimeMillis :
+ mOriginalDuration - (time - mStartTimeMillis));
+ mTransitionState = TRANSITION_STARTING;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ boolean done = true;
+
+ switch (mTransitionState) {
+ case TRANSITION_STARTING:
+ mStartTimeMillis = SystemClock.uptimeMillis();
+ done = false;
+ mTransitionState = TRANSITION_RUNNING;
+ break;
+
+ case TRANSITION_RUNNING:
+ if (mStartTimeMillis >= 0) {
+ float normalized = (float)
+ (SystemClock.uptimeMillis() - mStartTimeMillis) / mDuration;
+ done = normalized >= 1.0f;
+ normalized = Math.min(normalized, 1.0f);
+ mAlpha = (int) (mFrom + (mTo - mFrom) * normalized);
+ }
+ break;
+ }
+
+ final int alpha = mAlpha;
+ final boolean crossFade = mCrossFade;
+ final ChildDrawable[] array = mLayerState.mChildren;
+
+ if (done) {
+ // the setAlpha() calls below trigger invalidation and redraw. If we're done, just draw
+ // the appropriate drawable[s] and return
+ if (!crossFade || alpha == 0) {
+ array[0].mDrawable.draw(canvas);
+ }
+ if (alpha == 0xFF) {
+ array[1].mDrawable.draw(canvas);
+ }
+ return;
+ }
+
+ Drawable d;
+ d = array[0].mDrawable;
+ if (crossFade) {
+ d.setAlpha(255 - alpha);
+ }
+ d.draw(canvas);
+ if (crossFade) {
+ d.setAlpha(0xFF);
+ }
+
+ if (alpha > 0) {
+ d = array[1].mDrawable;
+ d.setAlpha(alpha);
+ d.draw(canvas);
+ d.setAlpha(0xFF);
+ }
+
+ if (!done) {
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Enables or disables the cross fade of the drawables. When cross fade
+ * is disabled, the first drawable is always drawn opaque. With cross
+ * fade enabled, the first drawable is drawn with the opposite alpha of
+ * the second drawable. Cross fade is disabled by default.
+ *
+ * @param enabled True to enable cross fading, false otherwise.
+ */
+ public void setCrossFadeEnabled(boolean enabled) {
+ mCrossFade = enabled;
+ }
+
+ /**
+ * Indicates whether the cross fade is enabled for this transition.
+ *
+ * @return True if cross fading is enabled, false otherwise.
+ */
+ public boolean isCrossFadeEnabled() {
+ return mCrossFade;
+ }
+
+ static class TransitionState extends LayerState {
+ TransitionState(TransitionState orig, TransitionDrawable owner, Resources res) {
+ super(orig, owner, res);
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new TransitionDrawable(this, (Resources) null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new TransitionDrawable(this, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations;
+ }
+ }
+}
diff --git a/android/graphics/drawable/VectorDrawable.java b/android/graphics/drawable/VectorDrawable.java
new file mode 100644
index 0000000..c3ef450
--- /dev/null
+++ b/android/graphics/drawable/VectorDrawable.java
@@ -0,0 +1,2291 @@
+/*
+ * Copyright (C) 2014 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 android.graphics.drawable;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ColorStateList;
+import android.content.res.ComplexColor;
+import android.content.res.GradientColor;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Insets;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.FloatProperty;
+import android.util.IntProperty;
+import android.util.LayoutDirection;
+import android.util.Log;
+import android.util.PathParser;
+import android.util.Property;
+import android.util.Xml;
+
+import com.android.internal.R;
+import com.android.internal.util.VirtualRefBasePtr;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Stack;
+
+import dalvik.annotation.optimization.FastNative;
+import dalvik.system.VMRuntime;
+
+/**
+ * This lets you create a drawable based on an XML vector graphic.
+ * <p/>
+ * <strong>Note:</strong> To optimize for the re-drawing performance, one bitmap cache is created
+ * for each VectorDrawable. Therefore, referring to the same VectorDrawable means sharing the same
+ * bitmap cache. If these references don't agree upon on the same size, the bitmap will be recreated
+ * and redrawn every time size is changed. In other words, if a VectorDrawable is used for
+ * different sizes, it is more efficient to create multiple VectorDrawables, one for each size.
+ * <p/>
+ * VectorDrawable can be defined in an XML file with the <code><vector></code> element.
+ * <p/>
+ * The vector drawable has the following elements:
+ * <p/>
+ * <dt><code><vector></code></dt>
+ * <dl>
+ * <dd>Used to define a vector drawable
+ * <dl>
+ * <dt><code>android:name</code></dt>
+ * <dd>Defines the name of this vector drawable.</dd>
+ * <dt><code>android:width</code></dt>
+ * <dd>Used to define the intrinsic width of the drawable.
+ * This support all the dimension units, normally specified with dp.</dd>
+ * <dt><code>android:height</code></dt>
+ * <dd>Used to define the intrinsic height the drawable.
+ * This support all the dimension units, normally specified with dp.</dd>
+ * <dt><code>android:viewportWidth</code></dt>
+ * <dd>Used to define the width of the viewport space. Viewport is basically
+ * the virtual canvas where the paths are drawn on.</dd>
+ * <dt><code>android:viewportHeight</code></dt>
+ * <dd>Used to define the height of the viewport space. Viewport is basically
+ * the virtual canvas where the paths are drawn on.</dd>
+ * <dt><code>android:tint</code></dt>
+ * <dd>The color to apply to the drawable as a tint. By default, no tint is applied.</dd>
+ * <dt><code>android:tintMode</code></dt>
+ * <dd>The Porter-Duff blending mode for the tint color. Default is src_in.</dd>
+ * <dt><code>android:autoMirrored</code></dt>
+ * <dd>Indicates if the drawable needs to be mirrored when its layout direction is
+ * RTL (right-to-left). Default is false.</dd>
+ * <dt><code>android:alpha</code></dt>
+ * <dd>The opacity of this drawable. Default is 1.0.</dd>
+ * </dl></dd>
+ * </dl>
+ *
+ * <dl>
+ * <dt><code><group></code></dt>
+ * <dd>Defines a group of paths or subgroups, plus transformation information.
+ * The transformations are defined in the same coordinates as the viewport.
+ * And the transformations are applied in the order of scale, rotate then translate.
+ * <dl>
+ * <dt><code>android:name</code></dt>
+ * <dd>Defines the name of the group.</dd>
+ * <dt><code>android:rotation</code></dt>
+ * <dd>The degrees of rotation of the group. Default is 0.</dd>
+ * <dt><code>android:pivotX</code></dt>
+ * <dd>The X coordinate of the pivot for the scale and rotation of the group.
+ * This is defined in the viewport space. Default is 0.</dd>
+ * <dt><code>android:pivotY</code></dt>
+ * <dd>The Y coordinate of the pivot for the scale and rotation of the group.
+ * This is defined in the viewport space. Default is 0.</dd>
+ * <dt><code>android:scaleX</code></dt>
+ * <dd>The amount of scale on the X Coordinate. Default is 1.</dd>
+ * <dt><code>android:scaleY</code></dt>
+ * <dd>The amount of scale on the Y coordinate. Default is 1.</dd>
+ * <dt><code>android:translateX</code></dt>
+ * <dd>The amount of translation on the X coordinate.
+ * This is defined in the viewport space. Default is 0.</dd>
+ * <dt><code>android:translateY</code></dt>
+ * <dd>The amount of translation on the Y coordinate.
+ * This is defined in the viewport space. Default is 0.</dd>
+ * </dl></dd>
+ * </dl>
+ *
+ * <dl>
+ * <dt><code><path></code></dt>
+ * <dd>Defines paths to be drawn.
+ * <dl>
+ * <dt><code>android:name</code></dt>
+ * <dd>Defines the name of the path.</dd>
+ * <dt><code>android:pathData</code></dt>
+ * <dd>Defines path data using exactly same format as "d" attribute
+ * in the SVG's path data. This is defined in the viewport space.</dd>
+ * <dt><code>android:fillColor</code></dt>
+ * <dd>Specifies the color used to fill the path. May be a color or, for SDK 24+, a color state list
+ * or a gradient color (See {@link android.R.styleable#GradientColor}
+ * and {@link android.R.styleable#GradientColorItem}).
+ * If this property is animated, any value set by the animation will override the original value.
+ * No path fill is drawn if this property is not specified.</dd>
+ * <dt><code>android:strokeColor</code></dt>
+ * <dd>Specifies the color used to draw the path outline. May be a color or, for SDK 24+, a color
+ * state list or a gradient color (See {@link android.R.styleable#GradientColor}
+ * and {@link android.R.styleable#GradientColorItem}).
+ * If this property is animated, any value set by the animation will override the original value.
+ * No path outline is drawn if this property is not specified.</dd>
+ * <dt><code>android:strokeWidth</code></dt>
+ * <dd>The width a path stroke. Default is 0.</dd>
+ * <dt><code>android:strokeAlpha</code></dt>
+ * <dd>The opacity of a path stroke. Default is 1.</dd>
+ * <dt><code>android:fillAlpha</code></dt>
+ * <dd>The opacity to fill the path with. Default is 1.</dd>
+ * <dt><code>android:trimPathStart</code></dt>
+ * <dd>The fraction of the path to trim from the start, in the range from 0 to 1. Default is 0.</dd>
+ * <dt><code>android:trimPathEnd</code></dt>
+ * <dd>The fraction of the path to trim from the end, in the range from 0 to 1. Default is 1.</dd>
+ * <dt><code>android:trimPathOffset</code></dt>
+ * <dd>Shift trim region (allows showed region to include the start and end), in the range
+ * from 0 to 1. Default is 0.</dd>
+ * <dt><code>android:strokeLineCap</code></dt>
+ * <dd>Sets the linecap for a stroked path: butt, round, square. Default is butt.</dd>
+ * <dt><code>android:strokeLineJoin</code></dt>
+ * <dd>Sets the lineJoin for a stroked path: miter,round,bevel. Default is miter.</dd>
+ * <dt><code>android:strokeMiterLimit</code></dt>
+ * <dd>Sets the Miter limit for a stroked path. Default is 4.</dd>
+ * <dt><code>android:fillType</code></dt>
+ * <dd>For SDK 24+, sets the fillType for a path. The types can be either "evenOdd" or "nonZero". They behave the
+ * same as SVG's "fill-rule" properties. Default is nonZero. For more details, see
+ * <a href="https://www.w3.org/TR/SVG/painting.html#FillRuleProperty">FillRuleProperty</a></dd>
+ * </dl></dd>
+ *
+ * </dl>
+ *
+ * <dl>
+ * <dt><code><clip-path></code></dt>
+ * <dd>Defines path to be the current clip. Note that the clip path only apply to
+ * the current group and its children.
+ * <dl>
+ * <dt><code>android:name</code></dt>
+ * <dd>Defines the name of the clip path.</dd>
+ * <dd>Animatable : No.</dd>
+ * <dt><code>android:pathData</code></dt>
+ * <dd>Defines clip path using the same format as "d" attribute
+ * in the SVG's path data.</dd>
+ * <dd>Animatable : Yes.</dd>
+ * </dl></dd>
+ * </dl>
+ * <li>Here is a simple VectorDrawable in this vectordrawable.xml file.
+ * <pre>
+ * <vector xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:height="64dp"
+ * android:width="64dp"
+ * android:viewportHeight="600"
+ * android:viewportWidth="600" >
+ * <group
+ * android:name="rotationGroup"
+ * android:pivotX="300.0"
+ * android:pivotY="300.0"
+ * android:rotation="45.0" >
+ * <path
+ * android:name="v"
+ * android:fillColor="#000000"
+ * android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
+ * </group>
+ * </vector>
+ * </pre>
+ * </li>
+ * <li>And here is an example of linear gradient color, which is supported in SDK 24+.
+ * See more details in {@link android.R.styleable#GradientColor} and
+ * {@link android.R.styleable#GradientColorItem}.
+ * <pre>
+ * <gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:angle="90"
+ * android:startColor="?android:attr/colorPrimary"
+ * android:endColor="?android:attr/colorControlActivated"
+ * android:centerColor="#f00"
+ * android:startX="0"
+ * android:startY="0"
+ * android:endX="100"
+ * android:endY="100"
+ * android:type="linear">
+ * </gradient>
+ * </pre>
+ * </li>
+ *
+ */
+
+public class VectorDrawable extends Drawable {
+ private static final String LOGTAG = VectorDrawable.class.getSimpleName();
+
+ private static final String SHAPE_CLIP_PATH = "clip-path";
+ private static final String SHAPE_GROUP = "group";
+ private static final String SHAPE_PATH = "path";
+ private static final String SHAPE_VECTOR = "vector";
+
+ private VectorDrawableState mVectorState;
+
+ private PorterDuffColorFilter mTintFilter;
+ private ColorFilter mColorFilter;
+
+ private boolean mMutated;
+
+ /** The density of the display on which this drawable will be rendered. */
+ private int mTargetDensity;
+
+ // Given the virtual display setup, the dpi can be different than the inflation's dpi.
+ // Therefore, we need to scale the values we got from the getDimension*().
+ private int mDpiScaledWidth = 0;
+ private int mDpiScaledHeight = 0;
+ private Insets mDpiScaledInsets = Insets.NONE;
+
+ /** Whether DPI-scaled width, height, and insets need to be updated. */
+ private boolean mDpiScaledDirty = true;
+
+ // Temp variable, only for saving "new" operation at the draw() time.
+ private final Rect mTmpBounds = new Rect();
+
+ public VectorDrawable() {
+ this(new VectorDrawableState(null), null);
+ }
+
+ /**
+ * The one constructor to rule them all. This is called by all public
+ * constructors to set the state and initialize local properties.
+ */
+ private VectorDrawable(@NonNull VectorDrawableState state, @Nullable Resources res) {
+ mVectorState = state;
+ updateLocalState(res);
+ }
+
+ /**
+ * Initializes local dynamic properties from state. This should be called
+ * after significant state changes, e.g. from the One True Constructor and
+ * after inflating or applying a theme.
+ *
+ * @param res resources of the context in which the drawable will be
+ * displayed, or {@code null} to use the constant state defaults
+ */
+ private void updateLocalState(Resources res) {
+ final int density = Drawable.resolveDensity(res, mVectorState.mDensity);
+ if (mTargetDensity != density) {
+ mTargetDensity = density;
+ mDpiScaledDirty = true;
+ }
+
+ mTintFilter = updateTintFilter(mTintFilter, mVectorState.mTint, mVectorState.mTintMode);
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mVectorState = new VectorDrawableState(mVectorState);
+ mMutated = true;
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearMutated() {
+ super.clearMutated();
+ mMutated = false;
+ }
+
+ Object getTargetByName(String name) {
+ return mVectorState.mVGTargetsMap.get(name);
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ mVectorState.mChangingConfigurations = getChangingConfigurations();
+ return mVectorState;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ // We will offset the bounds for drawBitmap, so copyBounds() here instead
+ // of getBounds().
+ copyBounds(mTmpBounds);
+ if (mTmpBounds.width() <= 0 || mTmpBounds.height() <= 0) {
+ // Nothing to draw
+ return;
+ }
+
+ // Color filters always override tint filters.
+ final ColorFilter colorFilter = (mColorFilter == null ? mTintFilter : mColorFilter);
+ final long colorFilterNativeInstance = colorFilter == null ? 0 :
+ colorFilter.getNativeInstance();
+ boolean canReuseCache = mVectorState.canReuseCache();
+ int pixelCount = nDraw(mVectorState.getNativeRenderer(), canvas.getNativeCanvasWrapper(),
+ colorFilterNativeInstance, mTmpBounds, needMirroring(),
+ canReuseCache);
+ if (pixelCount == 0) {
+ // Invalid canvas matrix or drawable bounds. This would not affect existing bitmap
+ // cache, if any.
+ return;
+ }
+
+ int deltaInBytes;
+ // Track different bitmap cache based whether the canvas is hw accelerated. By doing so,
+ // we don't over count bitmap cache allocation: if the input canvas is always of the same
+ // type, only one bitmap cache is allocated.
+ if (canvas.isHardwareAccelerated()) {
+ // Each pixel takes 4 bytes.
+ deltaInBytes = (pixelCount - mVectorState.mLastHWCachePixelCount) * 4;
+ mVectorState.mLastHWCachePixelCount = pixelCount;
+ } else {
+ // Each pixel takes 4 bytes.
+ deltaInBytes = (pixelCount - mVectorState.mLastSWCachePixelCount) * 4;
+ mVectorState.mLastSWCachePixelCount = pixelCount;
+ }
+ if (deltaInBytes > 0) {
+ VMRuntime.getRuntime().registerNativeAllocation(deltaInBytes);
+ } else if (deltaInBytes < 0) {
+ VMRuntime.getRuntime().registerNativeFree(-deltaInBytes);
+ }
+ }
+
+
+ @Override
+ public int getAlpha() {
+ return (int) (mVectorState.getAlpha() * 255);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (mVectorState.setAlpha(alpha / 255f)) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mColorFilter = colorFilter;
+ invalidateSelf();
+ }
+
+ @Override
+ public ColorFilter getColorFilter() {
+ return mColorFilter;
+ }
+
+ @Override
+ public void setTintList(ColorStateList tint) {
+ final VectorDrawableState state = mVectorState;
+ if (state.mTint != tint) {
+ state.mTint = tint;
+ mTintFilter = updateTintFilter(mTintFilter, tint, state.mTintMode);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setTintMode(Mode tintMode) {
+ final VectorDrawableState state = mVectorState;
+ if (state.mTintMode != tintMode) {
+ state.mTintMode = tintMode;
+ mTintFilter = updateTintFilter(mTintFilter, state.mTint, tintMode);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public boolean isStateful() {
+ return super.isStateful() || (mVectorState != null && mVectorState.isStateful());
+ }
+
+ /** @hide */
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return mVectorState != null && mVectorState.hasFocusStateSpecified();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] stateSet) {
+ boolean changed = false;
+
+ // When the VD is stateful, we need to mutate the drawable such that we don't share the
+ // cache bitmap with others. Such that the state change only affect this new cached bitmap.
+ if (isStateful()) {
+ mutate();
+ }
+ final VectorDrawableState state = mVectorState;
+ if (state.onStateChange(stateSet)) {
+ changed = true;
+ state.mCacheDirty = true;
+ }
+ if (state.mTint != null && state.mTintMode != null) {
+ mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
+ changed = true;
+ }
+
+ return changed;
+ }
+
+ @Override
+ public int getOpacity() {
+ // We can't tell whether the drawable is fully opaque unless we examine all the pixels,
+ // but we could tell it is transparent if the root alpha is 0.
+ return getAlpha() == 0 ? PixelFormat.TRANSPARENT : PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ if (mDpiScaledDirty) {
+ computeVectorSize();
+ }
+ return mDpiScaledWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ if (mDpiScaledDirty) {
+ computeVectorSize();
+ }
+ return mDpiScaledHeight;
+ }
+
+ /** @hide */
+ @Override
+ public Insets getOpticalInsets() {
+ if (mDpiScaledDirty) {
+ computeVectorSize();
+ }
+ return mDpiScaledInsets;
+ }
+
+ /*
+ * Update local dimensions to adjust for a target density that may differ
+ * from the source density against which the constant state was loaded.
+ */
+ void computeVectorSize() {
+ final Insets opticalInsets = mVectorState.mOpticalInsets;
+
+ final int sourceDensity = mVectorState.mDensity;
+ final int targetDensity = mTargetDensity;
+ if (targetDensity != sourceDensity) {
+ mDpiScaledWidth = Drawable.scaleFromDensity(mVectorState.mBaseWidth, sourceDensity,
+ targetDensity, true);
+ mDpiScaledHeight = Drawable.scaleFromDensity(mVectorState.mBaseHeight,sourceDensity,
+ targetDensity, true);
+ final int left = Drawable.scaleFromDensity(
+ opticalInsets.left, sourceDensity, targetDensity, false);
+ final int right = Drawable.scaleFromDensity(
+ opticalInsets.right, sourceDensity, targetDensity, false);
+ final int top = Drawable.scaleFromDensity(
+ opticalInsets.top, sourceDensity, targetDensity, false);
+ final int bottom = Drawable.scaleFromDensity(
+ opticalInsets.bottom, sourceDensity, targetDensity, false);
+ mDpiScaledInsets = Insets.of(left, top, right, bottom);
+ } else {
+ mDpiScaledWidth = mVectorState.mBaseWidth;
+ mDpiScaledHeight = mVectorState.mBaseHeight;
+ mDpiScaledInsets = opticalInsets;
+ }
+
+ mDpiScaledDirty = false;
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return (mVectorState != null && mVectorState.canApplyTheme()) || super.canApplyTheme();
+ }
+
+ @Override
+ public void applyTheme(Theme t) {
+ super.applyTheme(t);
+
+ final VectorDrawableState state = mVectorState;
+ if (state == null) {
+ return;
+ }
+
+ final boolean changedDensity = mVectorState.setDensity(
+ Drawable.resolveDensity(t.getResources(), 0));
+ mDpiScaledDirty |= changedDensity;
+
+ if (state.mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(
+ state.mThemeAttrs, R.styleable.VectorDrawable);
+ try {
+ state.mCacheDirty = true;
+ updateStateFromTypedArray(a);
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException(e);
+ } finally {
+ a.recycle();
+ }
+
+ // May have changed size.
+ mDpiScaledDirty = true;
+ }
+
+ // Apply theme to contained color state list.
+ if (state.mTint != null && state.mTint.canApplyTheme()) {
+ state.mTint = state.mTint.obtainForTheme(t);
+ }
+
+ if (mVectorState != null && mVectorState.canApplyTheme()) {
+ mVectorState.applyTheme(t);
+ }
+
+ // Update local properties.
+ updateLocalState(t.getResources());
+ }
+
+ /**
+ * The size of a pixel when scaled from the intrinsic dimension to the viewport dimension.
+ * This is used to calculate the path animation accuracy.
+ *
+ * @hide
+ */
+ public float getPixelSize() {
+ if (mVectorState == null ||
+ mVectorState.mBaseWidth == 0 ||
+ mVectorState.mBaseHeight == 0 ||
+ mVectorState.mViewportHeight == 0 ||
+ mVectorState.mViewportWidth == 0) {
+ return 1; // fall back to 1:1 pixel mapping.
+ }
+ float intrinsicWidth = mVectorState.mBaseWidth;
+ float intrinsicHeight = mVectorState.mBaseHeight;
+ float viewportWidth = mVectorState.mViewportWidth;
+ float viewportHeight = mVectorState.mViewportHeight;
+ float scaleX = viewportWidth / intrinsicWidth;
+ float scaleY = viewportHeight / intrinsicHeight;
+ return Math.min(scaleX, scaleY);
+ }
+
+ /** @hide */
+ public static VectorDrawable create(Resources resources, int rid) {
+ try {
+ final XmlPullParser parser = resources.getXml(rid);
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ int type;
+ while ((type=parser.next()) != XmlPullParser.START_TAG &&
+ type != XmlPullParser.END_DOCUMENT) {
+ // Empty loop
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException("No start tag found");
+ }
+
+ final VectorDrawable drawable = new VectorDrawable();
+ drawable.inflate(resources, parser, attrs);
+
+ return drawable;
+ } catch (XmlPullParserException e) {
+ Log.e(LOGTAG, "parser error", e);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "parser error", e);
+ }
+ return null;
+ }
+
+ @Override
+ public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, @Nullable Theme theme)
+ throws XmlPullParserException, IOException {
+ if (mVectorState.mRootGroup != null || mVectorState.mNativeTree != null) {
+ // This VD has been used to display other VD resource content, clean up.
+ if (mVectorState.mRootGroup != null) {
+ // Subtract the native allocation for all the nodes.
+ VMRuntime.getRuntime().registerNativeFree(mVectorState.mRootGroup.getNativeSize());
+ // Remove child nodes' reference to tree
+ mVectorState.mRootGroup.setTree(null);
+ }
+ mVectorState.mRootGroup = new VGroup();
+ if (mVectorState.mNativeTree != null) {
+ // Subtract the native allocation for the tree wrapper, which contains root node
+ // as well as rendering related data.
+ VMRuntime.getRuntime().registerNativeFree(mVectorState.NATIVE_ALLOCATION_SIZE);
+ mVectorState.mNativeTree.release();
+ }
+ mVectorState.createNativeTree(mVectorState.mRootGroup);
+ }
+ final VectorDrawableState state = mVectorState;
+ state.setDensity(Drawable.resolveDensity(r, 0));
+
+ final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
+
+ mDpiScaledDirty = true;
+
+ state.mCacheDirty = true;
+ inflateChildElements(r, parser, attrs, theme);
+
+ state.onTreeConstructionFinished();
+ // Update local properties.
+ updateLocalState(r);
+ }
+
+ private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
+ final VectorDrawableState state = mVectorState;
+
+ // Account for any configuration changes.
+ state.mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ state.mThemeAttrs = a.extractThemeAttrs();
+
+ final int tintMode = a.getInt(R.styleable.VectorDrawable_tintMode, -1);
+ if (tintMode != -1) {
+ state.mTintMode = Drawable.parseTintMode(tintMode, Mode.SRC_IN);
+ }
+
+ final ColorStateList tint = a.getColorStateList(R.styleable.VectorDrawable_tint);
+ if (tint != null) {
+ state.mTint = tint;
+ }
+
+ state.mAutoMirrored = a.getBoolean(
+ R.styleable.VectorDrawable_autoMirrored, state.mAutoMirrored);
+
+ float viewportWidth = a.getFloat(
+ R.styleable.VectorDrawable_viewportWidth, state.mViewportWidth);
+ float viewportHeight = a.getFloat(
+ R.styleable.VectorDrawable_viewportHeight, state.mViewportHeight);
+ state.setViewportSize(viewportWidth, viewportHeight);
+
+ if (state.mViewportWidth <= 0) {
+ throw new XmlPullParserException(a.getPositionDescription() +
+ "<vector> tag requires viewportWidth > 0");
+ } else if (state.mViewportHeight <= 0) {
+ throw new XmlPullParserException(a.getPositionDescription() +
+ "<vector> tag requires viewportHeight > 0");
+ }
+
+ state.mBaseWidth = a.getDimensionPixelSize(
+ R.styleable.VectorDrawable_width, state.mBaseWidth);
+ state.mBaseHeight = a.getDimensionPixelSize(
+ R.styleable.VectorDrawable_height, state.mBaseHeight);
+
+ if (state.mBaseWidth <= 0) {
+ throw new XmlPullParserException(a.getPositionDescription() +
+ "<vector> tag requires width > 0");
+ } else if (state.mBaseHeight <= 0) {
+ throw new XmlPullParserException(a.getPositionDescription() +
+ "<vector> tag requires height > 0");
+ }
+
+ final int insetLeft = a.getDimensionPixelOffset(
+ R.styleable.VectorDrawable_opticalInsetLeft, state.mOpticalInsets.left);
+ final int insetTop = a.getDimensionPixelOffset(
+ R.styleable.VectorDrawable_opticalInsetTop, state.mOpticalInsets.top);
+ final int insetRight = a.getDimensionPixelOffset(
+ R.styleable.VectorDrawable_opticalInsetRight, state.mOpticalInsets.right);
+ final int insetBottom = a.getDimensionPixelOffset(
+ R.styleable.VectorDrawable_opticalInsetBottom, state.mOpticalInsets.bottom);
+ state.mOpticalInsets = Insets.of(insetLeft, insetTop, insetRight, insetBottom);
+
+ final float alphaInFloat = a.getFloat(
+ R.styleable.VectorDrawable_alpha, state.getAlpha());
+ state.setAlpha(alphaInFloat);
+
+ final String name = a.getString(R.styleable.VectorDrawable_name);
+ if (name != null) {
+ state.mRootName = name;
+ state.mVGTargetsMap.put(name, state);
+ }
+ }
+
+ private void inflateChildElements(Resources res, XmlPullParser parser, AttributeSet attrs,
+ Theme theme) throws XmlPullParserException, IOException {
+ final VectorDrawableState state = mVectorState;
+ boolean noPathTag = true;
+
+ // Use a stack to help to build the group tree.
+ // The top of the stack is always the current group.
+ final Stack<VGroup> groupStack = new Stack<VGroup>();
+ groupStack.push(state.mRootGroup);
+
+ int eventType = parser.getEventType();
+ final int innerDepth = parser.getDepth() + 1;
+
+ // Parse everything until the end of the vector element.
+ while (eventType != XmlPullParser.END_DOCUMENT
+ && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
+ if (eventType == XmlPullParser.START_TAG) {
+ final String tagName = parser.getName();
+ final VGroup currentGroup = groupStack.peek();
+
+ if (SHAPE_PATH.equals(tagName)) {
+ final VFullPath path = new VFullPath();
+ path.inflate(res, attrs, theme);
+ currentGroup.addChild(path);
+ if (path.getPathName() != null) {
+ state.mVGTargetsMap.put(path.getPathName(), path);
+ }
+ noPathTag = false;
+ state.mChangingConfigurations |= path.mChangingConfigurations;
+ } else if (SHAPE_CLIP_PATH.equals(tagName)) {
+ final VClipPath path = new VClipPath();
+ path.inflate(res, attrs, theme);
+ currentGroup.addChild(path);
+ if (path.getPathName() != null) {
+ state.mVGTargetsMap.put(path.getPathName(), path);
+ }
+ state.mChangingConfigurations |= path.mChangingConfigurations;
+ } else if (SHAPE_GROUP.equals(tagName)) {
+ VGroup newChildGroup = new VGroup();
+ newChildGroup.inflate(res, attrs, theme);
+ currentGroup.addChild(newChildGroup);
+ groupStack.push(newChildGroup);
+ if (newChildGroup.getGroupName() != null) {
+ state.mVGTargetsMap.put(newChildGroup.getGroupName(),
+ newChildGroup);
+ }
+ state.mChangingConfigurations |= newChildGroup.mChangingConfigurations;
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ final String tagName = parser.getName();
+ if (SHAPE_GROUP.equals(tagName)) {
+ groupStack.pop();
+ }
+ }
+ eventType = parser.next();
+ }
+
+ if (noPathTag) {
+ final StringBuffer tag = new StringBuffer();
+
+ if (tag.length() > 0) {
+ tag.append(" or ");
+ }
+ tag.append(SHAPE_PATH);
+
+ throw new XmlPullParserException("no " + tag + " defined");
+ }
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return super.getChangingConfigurations() | mVectorState.getChangingConfigurations();
+ }
+
+ void setAllowCaching(boolean allowCaching) {
+ nSetAllowCaching(mVectorState.getNativeRenderer(), allowCaching);
+ }
+
+ private boolean needMirroring() {
+ return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL;
+ }
+
+ @Override
+ public void setAutoMirrored(boolean mirrored) {
+ if (mVectorState.mAutoMirrored != mirrored) {
+ mVectorState.mAutoMirrored = mirrored;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public boolean isAutoMirrored() {
+ return mVectorState.mAutoMirrored;
+ }
+
+ /**
+ * @hide
+ */
+ public long getNativeTree() {
+ return mVectorState.getNativeRenderer();
+ }
+
+ static class VectorDrawableState extends ConstantState {
+ // Variables below need to be copied (deep copy if applicable) for mutation.
+ int[] mThemeAttrs;
+ @Config int mChangingConfigurations;
+ ColorStateList mTint = null;
+ Mode mTintMode = DEFAULT_TINT_MODE;
+ boolean mAutoMirrored;
+
+ int mBaseWidth = 0;
+ int mBaseHeight = 0;
+ float mViewportWidth = 0;
+ float mViewportHeight = 0;
+ Insets mOpticalInsets = Insets.NONE;
+ String mRootName = null;
+ VGroup mRootGroup;
+ VirtualRefBasePtr mNativeTree = null;
+
+ int mDensity = DisplayMetrics.DENSITY_DEFAULT;
+ final ArrayMap<String, Object> mVGTargetsMap = new ArrayMap<>();
+
+ // Fields for cache
+ int[] mCachedThemeAttrs;
+ ColorStateList mCachedTint;
+ Mode mCachedTintMode;
+ boolean mCachedAutoMirrored;
+ boolean mCacheDirty;
+
+ // Since sw canvas and hw canvas uses different bitmap caches, we track the allocation of
+ // these bitmaps separately.
+ int mLastSWCachePixelCount = 0;
+ int mLastHWCachePixelCount = 0;
+
+ final static Property<VectorDrawableState, Float> ALPHA =
+ new FloatProperty<VectorDrawableState>("alpha") {
+ @Override
+ public void setValue(VectorDrawableState state, float value) {
+ state.setAlpha(value);
+ }
+
+ @Override
+ public Float get(VectorDrawableState state) {
+ return state.getAlpha();
+ }
+ };
+
+ Property getProperty(String propertyName) {
+ if (ALPHA.getName().equals(propertyName)) {
+ return ALPHA;
+ }
+ return null;
+ }
+
+ // This tracks the total native allocation for all the nodes.
+ private int mAllocationOfAllNodes = 0;
+
+ private static final int NATIVE_ALLOCATION_SIZE = 316;
+
+ // If copy is not null, deep copy the given VectorDrawableState. Otherwise, create a
+ // native vector drawable tree with an empty root group.
+ public VectorDrawableState(VectorDrawableState copy) {
+ if (copy != null) {
+ mThemeAttrs = copy.mThemeAttrs;
+ mChangingConfigurations = copy.mChangingConfigurations;
+ mTint = copy.mTint;
+ mTintMode = copy.mTintMode;
+ mAutoMirrored = copy.mAutoMirrored;
+ mRootGroup = new VGroup(copy.mRootGroup, mVGTargetsMap);
+ createNativeTreeFromCopy(copy, mRootGroup);
+
+ mBaseWidth = copy.mBaseWidth;
+ mBaseHeight = copy.mBaseHeight;
+ setViewportSize(copy.mViewportWidth, copy.mViewportHeight);
+ mOpticalInsets = copy.mOpticalInsets;
+
+ mRootName = copy.mRootName;
+ mDensity = copy.mDensity;
+ if (copy.mRootName != null) {
+ mVGTargetsMap.put(copy.mRootName, this);
+ }
+ } else {
+ mRootGroup = new VGroup();
+ createNativeTree(mRootGroup);
+ }
+ onTreeConstructionFinished();
+ }
+
+ private void createNativeTree(VGroup rootGroup) {
+ mNativeTree = new VirtualRefBasePtr(nCreateTree(rootGroup.mNativePtr));
+ // Register tree size
+ VMRuntime.getRuntime().registerNativeAllocation(NATIVE_ALLOCATION_SIZE);
+ }
+
+ // Create a new native tree with the given root group, and copy the properties from the
+ // given VectorDrawableState's native tree.
+ private void createNativeTreeFromCopy(VectorDrawableState copy, VGroup rootGroup) {
+ mNativeTree = new VirtualRefBasePtr(nCreateTreeFromCopy(
+ copy.mNativeTree.get(), rootGroup.mNativePtr));
+ // Register tree size
+ VMRuntime.getRuntime().registerNativeAllocation(NATIVE_ALLOCATION_SIZE);
+ }
+
+ // This should be called every time after a new RootGroup and all its subtrees are created
+ // (i.e. in constructors of VectorDrawableState and in inflate).
+ void onTreeConstructionFinished() {
+ mRootGroup.setTree(mNativeTree);
+ mAllocationOfAllNodes = mRootGroup.getNativeSize();
+ VMRuntime.getRuntime().registerNativeAllocation(mAllocationOfAllNodes);
+ }
+
+ long getNativeRenderer() {
+ if (mNativeTree == null) {
+ return 0;
+ }
+ return mNativeTree.get();
+ }
+
+ public boolean canReuseCache() {
+ if (!mCacheDirty
+ && mCachedThemeAttrs == mThemeAttrs
+ && mCachedTint == mTint
+ && mCachedTintMode == mTintMode
+ && mCachedAutoMirrored == mAutoMirrored) {
+ return true;
+ }
+ updateCacheStates();
+ return false;
+ }
+
+ public void updateCacheStates() {
+ // Use shallow copy here and shallow comparison in canReuseCache(),
+ // likely hit cache miss more, but practically not much difference.
+ mCachedThemeAttrs = mThemeAttrs;
+ mCachedTint = mTint;
+ mCachedTintMode = mTintMode;
+ mCachedAutoMirrored = mAutoMirrored;
+ mCacheDirty = false;
+ }
+
+ public void applyTheme(Theme t) {
+ mRootGroup.applyTheme(t);
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return mThemeAttrs != null
+ || (mRootGroup != null && mRootGroup.canApplyTheme())
+ || (mTint != null && mTint.canApplyTheme())
+ || super.canApplyTheme();
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new VectorDrawable(this, null);
+ }
+
+ @Override
+ public Drawable newDrawable(Resources res) {
+ return new VectorDrawable(this, res);
+ }
+
+ @Override
+ public @Config int getChangingConfigurations() {
+ return mChangingConfigurations
+ | (mTint != null ? mTint.getChangingConfigurations() : 0);
+ }
+
+ public boolean isStateful() {
+ return (mTint != null && mTint.isStateful())
+ || (mRootGroup != null && mRootGroup.isStateful());
+ }
+
+ public boolean hasFocusStateSpecified() {
+ return mTint != null && mTint.hasFocusStateSpecified()
+ || (mRootGroup != null && mRootGroup.hasFocusStateSpecified());
+ }
+
+ void setViewportSize(float viewportWidth, float viewportHeight) {
+ mViewportWidth = viewportWidth;
+ mViewportHeight = viewportHeight;
+ nSetRendererViewportSize(getNativeRenderer(), viewportWidth, viewportHeight);
+ }
+
+ public final boolean setDensity(int targetDensity) {
+ if (mDensity != targetDensity) {
+ final int sourceDensity = mDensity;
+ mDensity = targetDensity;
+ applyDensityScaling(sourceDensity, targetDensity);
+ return true;
+ }
+ return false;
+ }
+
+ private void applyDensityScaling(int sourceDensity, int targetDensity) {
+ mBaseWidth = Drawable.scaleFromDensity(mBaseWidth, sourceDensity, targetDensity, true);
+ mBaseHeight = Drawable.scaleFromDensity(mBaseHeight, sourceDensity, targetDensity,
+ true);
+
+ final int insetLeft = Drawable.scaleFromDensity(
+ mOpticalInsets.left, sourceDensity, targetDensity, false);
+ final int insetTop = Drawable.scaleFromDensity(
+ mOpticalInsets.top, sourceDensity, targetDensity, false);
+ final int insetRight = Drawable.scaleFromDensity(
+ mOpticalInsets.right, sourceDensity, targetDensity, false);
+ final int insetBottom = Drawable.scaleFromDensity(
+ mOpticalInsets.bottom, sourceDensity, targetDensity, false);
+ mOpticalInsets = Insets.of(insetLeft, insetTop, insetRight, insetBottom);
+ }
+
+ public boolean onStateChange(int[] stateSet) {
+ return mRootGroup.onStateChange(stateSet);
+ }
+
+ @Override
+ public void finalize() throws Throwable {
+ super.finalize();
+ int bitmapCacheSize = mLastHWCachePixelCount * 4 + mLastSWCachePixelCount * 4;
+ VMRuntime.getRuntime().registerNativeFree(NATIVE_ALLOCATION_SIZE
+ + mAllocationOfAllNodes + bitmapCacheSize);
+ }
+
+ /**
+ * setAlpha() and getAlpha() are used mostly for animation purpose. Return true if alpha
+ * has changed.
+ */
+ public boolean setAlpha(float alpha) {
+ return nSetRootAlpha(mNativeTree.get(), alpha);
+ }
+
+ @SuppressWarnings("unused")
+ public float getAlpha() {
+ return nGetRootAlpha(mNativeTree.get());
+ }
+ }
+
+ static class VGroup extends VObject {
+ private static final int ROTATION_INDEX = 0;
+ private static final int PIVOT_X_INDEX = 1;
+ private static final int PIVOT_Y_INDEX = 2;
+ private static final int SCALE_X_INDEX = 3;
+ private static final int SCALE_Y_INDEX = 4;
+ private static final int TRANSLATE_X_INDEX = 5;
+ private static final int TRANSLATE_Y_INDEX = 6;
+ private static final int TRANSFORM_PROPERTY_COUNT = 7;
+
+ private static final int NATIVE_ALLOCATION_SIZE = 100;
+
+ private static final HashMap<String, Integer> sPropertyIndexMap =
+ new HashMap<String, Integer>() {
+ {
+ put("translateX", TRANSLATE_X_INDEX);
+ put("translateY", TRANSLATE_Y_INDEX);
+ put("scaleX", SCALE_X_INDEX);
+ put("scaleY", SCALE_Y_INDEX);
+ put("pivotX", PIVOT_X_INDEX);
+ put("pivotY", PIVOT_Y_INDEX);
+ put("rotation", ROTATION_INDEX);
+ }
+ };
+
+ static int getPropertyIndex(String propertyName) {
+ if (sPropertyIndexMap.containsKey(propertyName)) {
+ return sPropertyIndexMap.get(propertyName);
+ } else {
+ // property not found
+ return -1;
+ }
+ }
+
+ // Below are the Properties that wrap the setters to avoid reflection overhead in animations
+ private static final Property<VGroup, Float> TRANSLATE_X =
+ new FloatProperty<VGroup> ("translateX") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setTranslateX(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getTranslateX();
+ }
+ };
+
+ private static final Property<VGroup, Float> TRANSLATE_Y =
+ new FloatProperty<VGroup> ("translateY") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setTranslateY(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getTranslateY();
+ }
+ };
+
+ private static final Property<VGroup, Float> SCALE_X =
+ new FloatProperty<VGroup> ("scaleX") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setScaleX(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getScaleX();
+ }
+ };
+
+ private static final Property<VGroup, Float> SCALE_Y =
+ new FloatProperty<VGroup> ("scaleY") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setScaleY(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getScaleY();
+ }
+ };
+
+ private static final Property<VGroup, Float> PIVOT_X =
+ new FloatProperty<VGroup> ("pivotX") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setPivotX(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getPivotX();
+ }
+ };
+
+ private static final Property<VGroup, Float> PIVOT_Y =
+ new FloatProperty<VGroup> ("pivotY") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setPivotY(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getPivotY();
+ }
+ };
+
+ private static final Property<VGroup, Float> ROTATION =
+ new FloatProperty<VGroup> ("rotation") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setRotation(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getRotation();
+ }
+ };
+
+ private static final HashMap<String, Property> sPropertyMap =
+ new HashMap<String, Property>() {
+ {
+ put("translateX", TRANSLATE_X);
+ put("translateY", TRANSLATE_Y);
+ put("scaleX", SCALE_X);
+ put("scaleY", SCALE_Y);
+ put("pivotX", PIVOT_X);
+ put("pivotY", PIVOT_Y);
+ put("rotation", ROTATION);
+ }
+ };
+ // Temp array to store transform values obtained from native.
+ private float[] mTransform;
+ /////////////////////////////////////////////////////
+ // Variables below need to be copied (deep copy if applicable) for mutation.
+ private final ArrayList<VObject> mChildren = new ArrayList<>();
+ private boolean mIsStateful;
+
+ // mLocalMatrix is updated based on the update of transformation information,
+ // either parsed from the XML or by animation.
+ private @Config int mChangingConfigurations;
+ private int[] mThemeAttrs;
+ private String mGroupName = null;
+
+ // The native object will be created in the constructor and will be destroyed in native
+ // when the neither java nor native has ref to the tree. This pointer should be valid
+ // throughout this VGroup Java object's life.
+ private final long mNativePtr;
+ public VGroup(VGroup copy, ArrayMap<String, Object> targetsMap) {
+
+ mIsStateful = copy.mIsStateful;
+ mThemeAttrs = copy.mThemeAttrs;
+ mGroupName = copy.mGroupName;
+ mChangingConfigurations = copy.mChangingConfigurations;
+ if (mGroupName != null) {
+ targetsMap.put(mGroupName, this);
+ }
+ mNativePtr = nCreateGroup(copy.mNativePtr);
+
+ final ArrayList<VObject> children = copy.mChildren;
+ for (int i = 0; i < children.size(); i++) {
+ final VObject copyChild = children.get(i);
+ if (copyChild instanceof VGroup) {
+ final VGroup copyGroup = (VGroup) copyChild;
+ addChild(new VGroup(copyGroup, targetsMap));
+ } else {
+ final VPath newPath;
+ if (copyChild instanceof VFullPath) {
+ newPath = new VFullPath((VFullPath) copyChild);
+ } else if (copyChild instanceof VClipPath) {
+ newPath = new VClipPath((VClipPath) copyChild);
+ } else {
+ throw new IllegalStateException("Unknown object in the tree!");
+ }
+ addChild(newPath);
+ if (newPath.mPathName != null) {
+ targetsMap.put(newPath.mPathName, newPath);
+ }
+ }
+ }
+ }
+
+ public VGroup() {
+ mNativePtr = nCreateGroup();
+ }
+
+ Property getProperty(String propertyName) {
+ if (sPropertyMap.containsKey(propertyName)) {
+ return sPropertyMap.get(propertyName);
+ } else {
+ // property not found
+ return null;
+ }
+ }
+
+ public String getGroupName() {
+ return mGroupName;
+ }
+
+ public void addChild(VObject child) {
+ nAddChild(mNativePtr, child.getNativePtr());
+ mChildren.add(child);
+ mIsStateful |= child.isStateful();
+ }
+
+ @Override
+ public void setTree(VirtualRefBasePtr treeRoot) {
+ super.setTree(treeRoot);
+ for (int i = 0; i < mChildren.size(); i++) {
+ mChildren.get(i).setTree(treeRoot);
+ }
+ }
+
+ @Override
+ public long getNativePtr() {
+ return mNativePtr;
+ }
+
+ @Override
+ public void inflate(Resources res, AttributeSet attrs, Theme theme) {
+ final TypedArray a = obtainAttributes(res, theme, attrs,
+ R.styleable.VectorDrawableGroup);
+ updateStateFromTypedArray(a);
+ a.recycle();
+ }
+
+ void updateStateFromTypedArray(TypedArray a) {
+ // Account for any configuration changes.
+ mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ mThemeAttrs = a.extractThemeAttrs();
+ if (mTransform == null) {
+ // Lazy initialization: If the group is created through copy constructor, this may
+ // never get called.
+ mTransform = new float[TRANSFORM_PROPERTY_COUNT];
+ }
+ boolean success = nGetGroupProperties(mNativePtr, mTransform, TRANSFORM_PROPERTY_COUNT);
+ if (!success) {
+ throw new RuntimeException("Error: inconsistent property count");
+ }
+ float rotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation,
+ mTransform[ROTATION_INDEX]);
+ float pivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX,
+ mTransform[PIVOT_X_INDEX]);
+ float pivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY,
+ mTransform[PIVOT_Y_INDEX]);
+ float scaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX,
+ mTransform[SCALE_X_INDEX]);
+ float scaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY,
+ mTransform[SCALE_Y_INDEX]);
+ float translateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX,
+ mTransform[TRANSLATE_X_INDEX]);
+ float translateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY,
+ mTransform[TRANSLATE_Y_INDEX]);
+
+ final String groupName = a.getString(R.styleable.VectorDrawableGroup_name);
+ if (groupName != null) {
+ mGroupName = groupName;
+ nSetName(mNativePtr, mGroupName);
+ }
+ nUpdateGroupProperties(mNativePtr, rotate, pivotX, pivotY, scaleX, scaleY,
+ translateX, translateY);
+ }
+
+ @Override
+ public boolean onStateChange(int[] stateSet) {
+ boolean changed = false;
+
+ final ArrayList<VObject> children = mChildren;
+ for (int i = 0, count = children.size(); i < count; i++) {
+ final VObject child = children.get(i);
+ if (child.isStateful()) {
+ changed |= child.onStateChange(stateSet);
+ }
+ }
+
+ return changed;
+ }
+
+ @Override
+ public boolean isStateful() {
+ return mIsStateful;
+ }
+
+ @Override
+ public boolean hasFocusStateSpecified() {
+ boolean result = false;
+
+ final ArrayList<VObject> children = mChildren;
+ for (int i = 0, count = children.size(); i < count; i++) {
+ final VObject child = children.get(i);
+ if (child.isStateful()) {
+ result |= child.hasFocusStateSpecified();
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ int getNativeSize() {
+ // Return the native allocation needed for the subtree.
+ int size = NATIVE_ALLOCATION_SIZE;
+ for (int i = 0; i < mChildren.size(); i++) {
+ size += mChildren.get(i).getNativeSize();
+ }
+ return size;
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ if (mThemeAttrs != null) {
+ return true;
+ }
+
+ final ArrayList<VObject> children = mChildren;
+ for (int i = 0, count = children.size(); i < count; i++) {
+ final VObject child = children.get(i);
+ if (child.canApplyTheme()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void applyTheme(Theme t) {
+ if (mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(mThemeAttrs,
+ R.styleable.VectorDrawableGroup);
+ updateStateFromTypedArray(a);
+ a.recycle();
+ }
+
+ final ArrayList<VObject> children = mChildren;
+ for (int i = 0, count = children.size(); i < count; i++) {
+ final VObject child = children.get(i);
+ if (child.canApplyTheme()) {
+ child.applyTheme(t);
+
+ // Applying a theme may have made the child stateful.
+ mIsStateful |= child.isStateful();
+ }
+ }
+ }
+
+ /* Setters and Getters, used by animator from AnimatedVectorDrawable. */
+ @SuppressWarnings("unused")
+ public float getRotation() {
+ return isTreeValid() ? nGetRotation(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ public void setRotation(float rotation) {
+ if (isTreeValid()) {
+ nSetRotation(mNativePtr, rotation);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public float getPivotX() {
+ return isTreeValid() ? nGetPivotX(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ public void setPivotX(float pivotX) {
+ if (isTreeValid()) {
+ nSetPivotX(mNativePtr, pivotX);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public float getPivotY() {
+ return isTreeValid() ? nGetPivotY(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ public void setPivotY(float pivotY) {
+ if (isTreeValid()) {
+ nSetPivotY(mNativePtr, pivotY);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public float getScaleX() {
+ return isTreeValid() ? nGetScaleX(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ public void setScaleX(float scaleX) {
+ if (isTreeValid()) {
+ nSetScaleX(mNativePtr, scaleX);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public float getScaleY() {
+ return isTreeValid() ? nGetScaleY(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ public void setScaleY(float scaleY) {
+ if (isTreeValid()) {
+ nSetScaleY(mNativePtr, scaleY);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public float getTranslateX() {
+ return isTreeValid() ? nGetTranslateX(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ public void setTranslateX(float translateX) {
+ if (isTreeValid()) {
+ nSetTranslateX(mNativePtr, translateX);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public float getTranslateY() {
+ return isTreeValid() ? nGetTranslateY(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ public void setTranslateY(float translateY) {
+ if (isTreeValid()) {
+ nSetTranslateY(mNativePtr, translateY);
+ }
+ }
+ }
+
+ /**
+ * Common Path information for clip path and normal path.
+ */
+ static abstract class VPath extends VObject {
+ protected PathParser.PathData mPathData = null;
+
+ String mPathName;
+ @Config int mChangingConfigurations;
+
+ private static final Property<VPath, PathParser.PathData> PATH_DATA =
+ new Property<VPath, PathParser.PathData>(PathParser.PathData.class, "pathData") {
+ @Override
+ public void set(VPath object, PathParser.PathData data) {
+ object.setPathData(data);
+ }
+
+ @Override
+ public PathParser.PathData get(VPath object) {
+ return object.getPathData();
+ }
+ };
+
+ Property getProperty(String propertyName) {
+ if (PATH_DATA.getName().equals(propertyName)) {
+ return PATH_DATA;
+ }
+ // property not found
+ return null;
+ }
+
+ public VPath() {
+ // Empty constructor.
+ }
+
+ public VPath(VPath copy) {
+ mPathName = copy.mPathName;
+ mChangingConfigurations = copy.mChangingConfigurations;
+ mPathData = copy.mPathData == null ? null : new PathParser.PathData(copy.mPathData);
+ }
+
+ public String getPathName() {
+ return mPathName;
+ }
+
+ /* Setters and Getters, used by animator from AnimatedVectorDrawable. */
+ @SuppressWarnings("unused")
+ public PathParser.PathData getPathData() {
+ return mPathData;
+ }
+
+ // TODO: Move the PathEvaluator and this setter and the getter above into native.
+ @SuppressWarnings("unused")
+ public void setPathData(PathParser.PathData pathData) {
+ mPathData.setPathData(pathData);
+ if (isTreeValid()) {
+ nSetPathData(getNativePtr(), mPathData.getNativePtr());
+ }
+ }
+ }
+
+ /**
+ * Clip path, which only has name and pathData.
+ */
+ private static class VClipPath extends VPath {
+ private final long mNativePtr;
+ private static final int NATIVE_ALLOCATION_SIZE = 120;
+
+ public VClipPath() {
+ mNativePtr = nCreateClipPath();
+ }
+
+ public VClipPath(VClipPath copy) {
+ super(copy);
+ mNativePtr = nCreateClipPath(copy.mNativePtr);
+ }
+
+ @Override
+ public long getNativePtr() {
+ return mNativePtr;
+ }
+
+ @Override
+ public void inflate(Resources r, AttributeSet attrs, Theme theme) {
+ final TypedArray a = obtainAttributes(r, theme, attrs,
+ R.styleable.VectorDrawableClipPath);
+ updateStateFromTypedArray(a);
+ a.recycle();
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ return false;
+ }
+
+ @Override
+ public void applyTheme(Theme theme) {
+ // No-op.
+ }
+
+ @Override
+ public boolean onStateChange(int[] stateSet) {
+ return false;
+ }
+
+ @Override
+ public boolean isStateful() {
+ return false;
+ }
+
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return false;
+ }
+
+ @Override
+ int getNativeSize() {
+ return NATIVE_ALLOCATION_SIZE;
+ }
+
+ private void updateStateFromTypedArray(TypedArray a) {
+ // Account for any configuration changes.
+ mChangingConfigurations |= a.getChangingConfigurations();
+
+ final String pathName = a.getString(R.styleable.VectorDrawableClipPath_name);
+ if (pathName != null) {
+ mPathName = pathName;
+ nSetName(mNativePtr, mPathName);
+ }
+
+ final String pathDataString = a.getString(R.styleable.VectorDrawableClipPath_pathData);
+ if (pathDataString != null) {
+ mPathData = new PathParser.PathData(pathDataString);
+ nSetPathString(mNativePtr, pathDataString, pathDataString.length());
+ }
+ }
+ }
+
+ /**
+ * Normal path, which contains all the fill / paint information.
+ */
+ static class VFullPath extends VPath {
+ private static final int STROKE_WIDTH_INDEX = 0;
+ private static final int STROKE_COLOR_INDEX = 1;
+ private static final int STROKE_ALPHA_INDEX = 2;
+ private static final int FILL_COLOR_INDEX = 3;
+ private static final int FILL_ALPHA_INDEX = 4;
+ private static final int TRIM_PATH_START_INDEX = 5;
+ private static final int TRIM_PATH_END_INDEX = 6;
+ private static final int TRIM_PATH_OFFSET_INDEX = 7;
+ private static final int STROKE_LINE_CAP_INDEX = 8;
+ private static final int STROKE_LINE_JOIN_INDEX = 9;
+ private static final int STROKE_MITER_LIMIT_INDEX = 10;
+ private static final int FILL_TYPE_INDEX = 11;
+ private static final int TOTAL_PROPERTY_COUNT = 12;
+
+ private static final int NATIVE_ALLOCATION_SIZE = 264;
+ // Property map for animatable attributes.
+ private final static HashMap<String, Integer> sPropertyIndexMap
+ = new HashMap<String, Integer> () {
+ {
+ put("strokeWidth", STROKE_WIDTH_INDEX);
+ put("strokeColor", STROKE_COLOR_INDEX);
+ put("strokeAlpha", STROKE_ALPHA_INDEX);
+ put("fillColor", FILL_COLOR_INDEX);
+ put("fillAlpha", FILL_ALPHA_INDEX);
+ put("trimPathStart", TRIM_PATH_START_INDEX);
+ put("trimPathEnd", TRIM_PATH_END_INDEX);
+ put("trimPathOffset", TRIM_PATH_OFFSET_INDEX);
+ }
+ };
+
+ // Below are the Properties that wrap the setters to avoid reflection overhead in animations
+ private static final Property<VFullPath, Float> STROKE_WIDTH =
+ new FloatProperty<VFullPath> ("strokeWidth") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setStrokeWidth(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getStrokeWidth();
+ }
+ };
+
+ private static final Property<VFullPath, Integer> STROKE_COLOR =
+ new IntProperty<VFullPath> ("strokeColor") {
+ @Override
+ public void setValue(VFullPath object, int value) {
+ object.setStrokeColor(value);
+ }
+
+ @Override
+ public Integer get(VFullPath object) {
+ return object.getStrokeColor();
+ }
+ };
+
+ private static final Property<VFullPath, Float> STROKE_ALPHA =
+ new FloatProperty<VFullPath> ("strokeAlpha") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setStrokeAlpha(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getStrokeAlpha();
+ }
+ };
+
+ private static final Property<VFullPath, Integer> FILL_COLOR =
+ new IntProperty<VFullPath>("fillColor") {
+ @Override
+ public void setValue(VFullPath object, int value) {
+ object.setFillColor(value);
+ }
+
+ @Override
+ public Integer get(VFullPath object) {
+ return object.getFillColor();
+ }
+ };
+
+ private static final Property<VFullPath, Float> FILL_ALPHA =
+ new FloatProperty<VFullPath> ("fillAlpha") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setFillAlpha(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getFillAlpha();
+ }
+ };
+
+ private static final Property<VFullPath, Float> TRIM_PATH_START =
+ new FloatProperty<VFullPath> ("trimPathStart") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setTrimPathStart(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getTrimPathStart();
+ }
+ };
+
+ private static final Property<VFullPath, Float> TRIM_PATH_END =
+ new FloatProperty<VFullPath> ("trimPathEnd") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setTrimPathEnd(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getTrimPathEnd();
+ }
+ };
+
+ private static final Property<VFullPath, Float> TRIM_PATH_OFFSET =
+ new FloatProperty<VFullPath> ("trimPathOffset") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setTrimPathOffset(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getTrimPathOffset();
+ }
+ };
+
+ private final static HashMap<String, Property> sPropertyMap
+ = new HashMap<String, Property> () {
+ {
+ put("strokeWidth", STROKE_WIDTH);
+ put("strokeColor", STROKE_COLOR);
+ put("strokeAlpha", STROKE_ALPHA);
+ put("fillColor", FILL_COLOR);
+ put("fillAlpha", FILL_ALPHA);
+ put("trimPathStart", TRIM_PATH_START);
+ put("trimPathEnd", TRIM_PATH_END);
+ put("trimPathOffset", TRIM_PATH_OFFSET);
+ }
+ };
+
+ // Temp array to store property data obtained from native getter.
+ private byte[] mPropertyData;
+ /////////////////////////////////////////////////////
+ // Variables below need to be copied (deep copy if applicable) for mutation.
+ private int[] mThemeAttrs;
+
+ ComplexColor mStrokeColors = null;
+ ComplexColor mFillColors = null;
+ private final long mNativePtr;
+
+ public VFullPath() {
+ mNativePtr = nCreateFullPath();
+ }
+
+ public VFullPath(VFullPath copy) {
+ super(copy);
+ mNativePtr = nCreateFullPath(copy.mNativePtr);
+ mThemeAttrs = copy.mThemeAttrs;
+ mStrokeColors = copy.mStrokeColors;
+ mFillColors = copy.mFillColors;
+ }
+
+ Property getProperty(String propertyName) {
+ Property p = super.getProperty(propertyName);
+ if (p != null) {
+ return p;
+ }
+ if (sPropertyMap.containsKey(propertyName)) {
+ return sPropertyMap.get(propertyName);
+ } else {
+ // property not found
+ return null;
+ }
+ }
+
+ int getPropertyIndex(String propertyName) {
+ if (!sPropertyIndexMap.containsKey(propertyName)) {
+ return -1;
+ } else {
+ return sPropertyIndexMap.get(propertyName);
+ }
+ }
+
+ @Override
+ public boolean onStateChange(int[] stateSet) {
+ boolean changed = false;
+
+ if (mStrokeColors != null && mStrokeColors instanceof ColorStateList) {
+ final int oldStrokeColor = getStrokeColor();
+ final int newStrokeColor =
+ ((ColorStateList) mStrokeColors).getColorForState(stateSet, oldStrokeColor);
+ changed |= oldStrokeColor != newStrokeColor;
+ if (oldStrokeColor != newStrokeColor) {
+ nSetStrokeColor(mNativePtr, newStrokeColor);
+ }
+ }
+
+ if (mFillColors != null && mFillColors instanceof ColorStateList) {
+ final int oldFillColor = getFillColor();
+ final int newFillColor = ((ColorStateList) mFillColors).getColorForState(stateSet, oldFillColor);
+ changed |= oldFillColor != newFillColor;
+ if (oldFillColor != newFillColor) {
+ nSetFillColor(mNativePtr, newFillColor);
+ }
+ }
+
+ return changed;
+ }
+
+ @Override
+ public boolean isStateful() {
+ return mStrokeColors != null || mFillColors != null;
+ }
+
+ @Override
+ public boolean hasFocusStateSpecified() {
+ return (mStrokeColors != null && mStrokeColors instanceof ColorStateList &&
+ ((ColorStateList) mStrokeColors).hasFocusStateSpecified()) &&
+ (mFillColors != null && mFillColors instanceof ColorStateList &&
+ ((ColorStateList) mFillColors).hasFocusStateSpecified());
+ }
+
+ @Override
+ int getNativeSize() {
+ return NATIVE_ALLOCATION_SIZE;
+ }
+
+ @Override
+ public long getNativePtr() {
+ return mNativePtr;
+ }
+
+ @Override
+ public void inflate(Resources r, AttributeSet attrs, Theme theme) {
+ final TypedArray a = obtainAttributes(r, theme, attrs,
+ R.styleable.VectorDrawablePath);
+ updateStateFromTypedArray(a);
+ a.recycle();
+ }
+
+ private void updateStateFromTypedArray(TypedArray a) {
+ int byteCount = TOTAL_PROPERTY_COUNT * 4;
+ if (mPropertyData == null) {
+ // Lazy initialization: If the path is created through copy constructor, this may
+ // never get called.
+ mPropertyData = new byte[byteCount];
+ }
+ // The bulk getters/setters of property data (e.g. stroke width, color, etc) allows us
+ // to pull current values from native and store modifications with only two methods,
+ // minimizing JNI overhead.
+ boolean success = nGetFullPathProperties(mNativePtr, mPropertyData, byteCount);
+ if (!success) {
+ throw new RuntimeException("Error: inconsistent property count");
+ }
+
+ ByteBuffer properties = ByteBuffer.wrap(mPropertyData);
+ properties.order(ByteOrder.nativeOrder());
+ float strokeWidth = properties.getFloat(STROKE_WIDTH_INDEX * 4);
+ int strokeColor = properties.getInt(STROKE_COLOR_INDEX * 4);
+ float strokeAlpha = properties.getFloat(STROKE_ALPHA_INDEX * 4);
+ int fillColor = properties.getInt(FILL_COLOR_INDEX * 4);
+ float fillAlpha = properties.getFloat(FILL_ALPHA_INDEX * 4);
+ float trimPathStart = properties.getFloat(TRIM_PATH_START_INDEX * 4);
+ float trimPathEnd = properties.getFloat(TRIM_PATH_END_INDEX * 4);
+ float trimPathOffset = properties.getFloat(TRIM_PATH_OFFSET_INDEX * 4);
+ int strokeLineCap = properties.getInt(STROKE_LINE_CAP_INDEX * 4);
+ int strokeLineJoin = properties.getInt(STROKE_LINE_JOIN_INDEX * 4);
+ float strokeMiterLimit = properties.getFloat(STROKE_MITER_LIMIT_INDEX * 4);
+ int fillType = properties.getInt(FILL_TYPE_INDEX * 4);
+ Shader fillGradient = null;
+ Shader strokeGradient = null;
+ // Account for any configuration changes.
+ mChangingConfigurations |= a.getChangingConfigurations();
+
+ // Extract the theme attributes, if any.
+ mThemeAttrs = a.extractThemeAttrs();
+
+ final String pathName = a.getString(R.styleable.VectorDrawablePath_name);
+ if (pathName != null) {
+ mPathName = pathName;
+ nSetName(mNativePtr, mPathName);
+ }
+
+ final String pathString = a.getString(R.styleable.VectorDrawablePath_pathData);
+ if (pathString != null) {
+ mPathData = new PathParser.PathData(pathString);
+ nSetPathString(mNativePtr, pathString, pathString.length());
+ }
+
+ final ComplexColor fillColors = a.getComplexColor(
+ R.styleable.VectorDrawablePath_fillColor);
+ if (fillColors != null) {
+ // If the colors is a gradient color, or the color state list is stateful, keep the
+ // colors information. Otherwise, discard the colors and keep the default color.
+ if (fillColors instanceof GradientColor) {
+ mFillColors = fillColors;
+ fillGradient = ((GradientColor) fillColors).getShader();
+ } else if (fillColors.isStateful()) {
+ mFillColors = fillColors;
+ } else {
+ mFillColors = null;
+ }
+ fillColor = fillColors.getDefaultColor();
+ }
+
+ final ComplexColor strokeColors = a.getComplexColor(
+ R.styleable.VectorDrawablePath_strokeColor);
+ if (strokeColors != null) {
+ // If the colors is a gradient color, or the color state list is stateful, keep the
+ // colors information. Otherwise, discard the colors and keep the default color.
+ if (strokeColors instanceof GradientColor) {
+ mStrokeColors = strokeColors;
+ strokeGradient = ((GradientColor) strokeColors).getShader();
+ } else if (strokeColors.isStateful()) {
+ mStrokeColors = strokeColors;
+ } else {
+ mStrokeColors = null;
+ }
+ strokeColor = strokeColors.getDefaultColor();
+ }
+ // Update the gradient info, even if the gradiet is null.
+ nUpdateFullPathFillGradient(mNativePtr,
+ fillGradient != null ? fillGradient.getNativeInstance() : 0);
+ nUpdateFullPathStrokeGradient(mNativePtr,
+ strokeGradient != null ? strokeGradient.getNativeInstance() : 0);
+
+ fillAlpha = a.getFloat(R.styleable.VectorDrawablePath_fillAlpha, fillAlpha);
+
+ strokeLineCap = a.getInt(
+ R.styleable.VectorDrawablePath_strokeLineCap, strokeLineCap);
+ strokeLineJoin = a.getInt(
+ R.styleable.VectorDrawablePath_strokeLineJoin, strokeLineJoin);
+ strokeMiterLimit = a.getFloat(
+ R.styleable.VectorDrawablePath_strokeMiterLimit, strokeMiterLimit);
+ strokeAlpha = a.getFloat(R.styleable.VectorDrawablePath_strokeAlpha,
+ strokeAlpha);
+ strokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth,
+ strokeWidth);
+ trimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd,
+ trimPathEnd);
+ trimPathOffset = a.getFloat(
+ R.styleable.VectorDrawablePath_trimPathOffset, trimPathOffset);
+ trimPathStart = a.getFloat(
+ R.styleable.VectorDrawablePath_trimPathStart, trimPathStart);
+ fillType = a.getInt(R.styleable.VectorDrawablePath_fillType, fillType);
+
+ nUpdateFullPathProperties(mNativePtr, strokeWidth, strokeColor, strokeAlpha,
+ fillColor, fillAlpha, trimPathStart, trimPathEnd, trimPathOffset,
+ strokeMiterLimit, strokeLineCap, strokeLineJoin, fillType);
+ }
+
+ @Override
+ public boolean canApplyTheme() {
+ if (mThemeAttrs != null) {
+ return true;
+ }
+
+ boolean fillCanApplyTheme = canComplexColorApplyTheme(mFillColors);
+ boolean strokeCanApplyTheme = canComplexColorApplyTheme(mStrokeColors);
+ if (fillCanApplyTheme || strokeCanApplyTheme) {
+ return true;
+ }
+ return false;
+
+ }
+
+ @Override
+ public void applyTheme(Theme t) {
+ // Resolve the theme attributes directly referred by the VectorDrawable.
+ if (mThemeAttrs != null) {
+ final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawablePath);
+ updateStateFromTypedArray(a);
+ a.recycle();
+ }
+
+ // Resolve the theme attributes in-directly referred by the VectorDrawable, for example,
+ // fillColor can refer to a color state list which itself needs to apply theme.
+ // And this is the reason we still want to keep partial update for the path's properties.
+ boolean fillCanApplyTheme = canComplexColorApplyTheme(mFillColors);
+ boolean strokeCanApplyTheme = canComplexColorApplyTheme(mStrokeColors);
+
+ if (fillCanApplyTheme) {
+ mFillColors = mFillColors.obtainForTheme(t);
+ if (mFillColors instanceof GradientColor) {
+ nUpdateFullPathFillGradient(mNativePtr,
+ ((GradientColor) mFillColors).getShader().getNativeInstance());
+ } else if (mFillColors instanceof ColorStateList) {
+ nSetFillColor(mNativePtr, mFillColors.getDefaultColor());
+ }
+ }
+
+ if (strokeCanApplyTheme) {
+ mStrokeColors = mStrokeColors.obtainForTheme(t);
+ if (mStrokeColors instanceof GradientColor) {
+ nUpdateFullPathStrokeGradient(mNativePtr,
+ ((GradientColor) mStrokeColors).getShader().getNativeInstance());
+ } else if (mStrokeColors instanceof ColorStateList) {
+ nSetStrokeColor(mNativePtr, mStrokeColors.getDefaultColor());
+ }
+ }
+ }
+
+ private boolean canComplexColorApplyTheme(ComplexColor complexColor) {
+ return complexColor != null && complexColor.canApplyTheme();
+ }
+
+ /* Setters and Getters, used by animator from AnimatedVectorDrawable. */
+ @SuppressWarnings("unused")
+ int getStrokeColor() {
+ return isTreeValid() ? nGetStrokeColor(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ void setStrokeColor(int strokeColor) {
+ mStrokeColors = null;
+ if (isTreeValid()) {
+ nSetStrokeColor(mNativePtr, strokeColor);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ float getStrokeWidth() {
+ return isTreeValid() ? nGetStrokeWidth(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ void setStrokeWidth(float strokeWidth) {
+ if (isTreeValid()) {
+ nSetStrokeWidth(mNativePtr, strokeWidth);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ float getStrokeAlpha() {
+ return isTreeValid() ? nGetStrokeAlpha(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ void setStrokeAlpha(float strokeAlpha) {
+ if (isTreeValid()) {
+ nSetStrokeAlpha(mNativePtr, strokeAlpha);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ int getFillColor() {
+ return isTreeValid() ? nGetFillColor(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ void setFillColor(int fillColor) {
+ mFillColors = null;
+ if (isTreeValid()) {
+ nSetFillColor(mNativePtr, fillColor);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ float getFillAlpha() {
+ return isTreeValid() ? nGetFillAlpha(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ void setFillAlpha(float fillAlpha) {
+ if (isTreeValid()) {
+ nSetFillAlpha(mNativePtr, fillAlpha);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ float getTrimPathStart() {
+ return isTreeValid() ? nGetTrimPathStart(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ void setTrimPathStart(float trimPathStart) {
+ if (isTreeValid()) {
+ nSetTrimPathStart(mNativePtr, trimPathStart);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ float getTrimPathEnd() {
+ return isTreeValid() ? nGetTrimPathEnd(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ void setTrimPathEnd(float trimPathEnd) {
+ if (isTreeValid()) {
+ nSetTrimPathEnd(mNativePtr, trimPathEnd);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ float getTrimPathOffset() {
+ return isTreeValid() ? nGetTrimPathOffset(mNativePtr) : 0;
+ }
+
+ @SuppressWarnings("unused")
+ void setTrimPathOffset(float trimPathOffset) {
+ if (isTreeValid()) {
+ nSetTrimPathOffset(mNativePtr, trimPathOffset);
+ }
+ }
+ }
+
+ abstract static class VObject {
+ VirtualRefBasePtr mTreePtr = null;
+ boolean isTreeValid() {
+ return mTreePtr != null && mTreePtr.get() != 0;
+ }
+ void setTree(VirtualRefBasePtr ptr) {
+ mTreePtr = ptr;
+ }
+ abstract long getNativePtr();
+ abstract void inflate(Resources r, AttributeSet attrs, Theme theme);
+ abstract boolean canApplyTheme();
+ abstract void applyTheme(Theme t);
+ abstract boolean onStateChange(int[] state);
+ abstract boolean isStateful();
+ abstract boolean hasFocusStateSpecified();
+ abstract int getNativeSize();
+ abstract Property getProperty(String propertyName);
+ }
+
+ private static native int nDraw(long rendererPtr, long canvasWrapperPtr,
+ long colorFilterPtr, Rect bounds, boolean needsMirroring, boolean canReuseCache);
+ private static native boolean nGetFullPathProperties(long pathPtr, byte[] properties,
+ int length);
+ private static native void nSetName(long nodePtr, String name);
+ private static native boolean nGetGroupProperties(long groupPtr, float[] properties,
+ int length);
+ private static native void nSetPathString(long pathPtr, String pathString, int length);
+
+ // ------------- @FastNative ------------------
+
+ @FastNative
+ private static native long nCreateTree(long rootGroupPtr);
+ @FastNative
+ private static native long nCreateTreeFromCopy(long treeToCopy, long rootGroupPtr);
+ @FastNative
+ private static native void nSetRendererViewportSize(long rendererPtr, float viewportWidth,
+ float viewportHeight);
+ @FastNative
+ private static native boolean nSetRootAlpha(long rendererPtr, float alpha);
+ @FastNative
+ private static native float nGetRootAlpha(long rendererPtr);
+ @FastNative
+ private static native void nSetAllowCaching(long rendererPtr, boolean allowCaching);
+
+ @FastNative
+ private static native long nCreateFullPath();
+ @FastNative
+ private static native long nCreateFullPath(long nativeFullPathPtr);
+
+ @FastNative
+ private static native void nUpdateFullPathProperties(long pathPtr, float strokeWidth,
+ int strokeColor, float strokeAlpha, int fillColor, float fillAlpha, float trimPathStart,
+ float trimPathEnd, float trimPathOffset, float strokeMiterLimit, int strokeLineCap,
+ int strokeLineJoin, int fillType);
+ @FastNative
+ private static native void nUpdateFullPathFillGradient(long pathPtr, long fillGradientPtr);
+ @FastNative
+ private static native void nUpdateFullPathStrokeGradient(long pathPtr, long strokeGradientPtr);
+
+ @FastNative
+ private static native long nCreateClipPath();
+ @FastNative
+ private static native long nCreateClipPath(long clipPathPtr);
+
+ @FastNative
+ private static native long nCreateGroup();
+ @FastNative
+ private static native long nCreateGroup(long groupPtr);
+ @FastNative
+ private static native void nUpdateGroupProperties(long groupPtr, float rotate, float pivotX,
+ float pivotY, float scaleX, float scaleY, float translateX, float translateY);
+
+ @FastNative
+ private static native void nAddChild(long groupPtr, long nodePtr);
+
+ /**
+ * The setters and getters below for paths and groups are here temporarily, and will be
+ * removed once the animation in AVD is replaced with RenderNodeAnimator, in which case the
+ * animation will modify these properties in native. By then no JNI hopping would be necessary
+ * for VD during animation, and these setters and getters will be obsolete.
+ */
+ // Setters and getters during animation.
+ @FastNative
+ private static native float nGetRotation(long groupPtr);
+ @FastNative
+ private static native void nSetRotation(long groupPtr, float rotation);
+ @FastNative
+ private static native float nGetPivotX(long groupPtr);
+ @FastNative
+ private static native void nSetPivotX(long groupPtr, float pivotX);
+ @FastNative
+ private static native float nGetPivotY(long groupPtr);
+ @FastNative
+ private static native void nSetPivotY(long groupPtr, float pivotY);
+ @FastNative
+ private static native float nGetScaleX(long groupPtr);
+ @FastNative
+ private static native void nSetScaleX(long groupPtr, float scaleX);
+ @FastNative
+ private static native float nGetScaleY(long groupPtr);
+ @FastNative
+ private static native void nSetScaleY(long groupPtr, float scaleY);
+ @FastNative
+ private static native float nGetTranslateX(long groupPtr);
+ @FastNative
+ private static native void nSetTranslateX(long groupPtr, float translateX);
+ @FastNative
+ private static native float nGetTranslateY(long groupPtr);
+ @FastNative
+ private static native void nSetTranslateY(long groupPtr, float translateY);
+
+ // Setters and getters for VPath during animation.
+ @FastNative
+ private static native void nSetPathData(long pathPtr, long pathDataPtr);
+ @FastNative
+ private static native float nGetStrokeWidth(long pathPtr);
+ @FastNative
+ private static native void nSetStrokeWidth(long pathPtr, float width);
+ @FastNative
+ private static native int nGetStrokeColor(long pathPtr);
+ @FastNative
+ private static native void nSetStrokeColor(long pathPtr, int strokeColor);
+ @FastNative
+ private static native float nGetStrokeAlpha(long pathPtr);
+ @FastNative
+ private static native void nSetStrokeAlpha(long pathPtr, float alpha);
+ @FastNative
+ private static native int nGetFillColor(long pathPtr);
+ @FastNative
+ private static native void nSetFillColor(long pathPtr, int fillColor);
+ @FastNative
+ private static native float nGetFillAlpha(long pathPtr);
+ @FastNative
+ private static native void nSetFillAlpha(long pathPtr, float fillAlpha);
+ @FastNative
+ private static native float nGetTrimPathStart(long pathPtr);
+ @FastNative
+ private static native void nSetTrimPathStart(long pathPtr, float trimPathStart);
+ @FastNative
+ private static native float nGetTrimPathEnd(long pathPtr);
+ @FastNative
+ private static native void nSetTrimPathEnd(long pathPtr, float trimPathEnd);
+ @FastNative
+ private static native float nGetTrimPathOffset(long pathPtr);
+ @FastNative
+ private static native void nSetTrimPathOffset(long pathPtr, float trimPathOffset);
+}
diff --git a/android/graphics/drawable/VectorDrawable_Delegate.java b/android/graphics/drawable/VectorDrawable_Delegate.java
new file mode 100644
index 0000000..5fa7102
--- /dev/null
+++ b/android/graphics/drawable/VectorDrawable_Delegate.java
@@ -0,0 +1,1276 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.drawable;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.NonNull;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.BaseCanvas_Delegate;
+import android.graphics.Canvas_Delegate;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Cap;
+import android.graphics.Paint.Join;
+import android.graphics.Paint_Delegate;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.Path_Delegate;
+import android.graphics.Rect;
+import android.graphics.Region.Op;
+import android.graphics.Shader_Delegate;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.PathParser_Delegate;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+import static android.graphics.Canvas.CLIP_SAVE_FLAG;
+import static android.graphics.Canvas.MATRIX_SAVE_FLAG;
+import static android.graphics.Paint.Cap.BUTT;
+import static android.graphics.Paint.Cap.ROUND;
+import static android.graphics.Paint.Cap.SQUARE;
+import static android.graphics.Paint.Join.BEVEL;
+import static android.graphics.Paint.Join.MITER;
+import static android.graphics.Paint.Style;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link VectorDrawable}
+ * <p>
+ * Through the layoutlib_create tool, the original methods of VectorDrawable have been replaced by
+ * calls to methods of the same name in this delegate class.
+ */
+@SuppressWarnings("unused")
+public class VectorDrawable_Delegate {
+ private static final String LOGTAG = VectorDrawable_Delegate.class.getSimpleName();
+ private static final boolean DBG_VECTOR_DRAWABLE = false;
+
+ private static final DelegateManager<VNativeObject> sPathManager =
+ new DelegateManager<>(VNativeObject.class);
+
+ private static long addNativeObject(VNativeObject object) {
+ long ptr = sPathManager.addNewDelegate(object);
+ object.setNativePtr(ptr);
+
+ return ptr;
+ }
+
+ /**
+ * Obtains styled attributes from the theme, if available, or unstyled resources if the theme is
+ * null.
+ */
+ private static TypedArray obtainAttributes(
+ Resources res, Theme theme, AttributeSet set, int[] attrs) {
+ if (theme == null) {
+ return res.obtainAttributes(set, attrs);
+ }
+ return theme.obtainStyledAttributes(set, attrs, 0, 0);
+ }
+
+ private static int applyAlpha(int color, float alpha) {
+ int alphaBytes = Color.alpha(color);
+ color &= 0x00FFFFFF;
+ color |= ((int) (alphaBytes * alpha)) << 24;
+ return color;
+ }
+
+ @LayoutlibDelegate
+ static long nCreateTree(long rootGroupPtr) {
+ return addNativeObject(new VPathRenderer_Delegate(rootGroupPtr));
+ }
+
+ @LayoutlibDelegate
+ static long nCreateTreeFromCopy(long rendererToCopyPtr, long rootGroupPtr) {
+ VPathRenderer_Delegate rendererToCopy = VNativeObject.getDelegate(rendererToCopyPtr);
+ return addNativeObject(new VPathRenderer_Delegate(rendererToCopy,
+ rootGroupPtr));
+ }
+
+ @LayoutlibDelegate
+ static void nSetRendererViewportSize(long rendererPtr, float viewportWidth,
+ float viewportHeight) {
+ VPathRenderer_Delegate nativePathRenderer = VNativeObject.getDelegate(rendererPtr);
+ nativePathRenderer.mViewportWidth = viewportWidth;
+ nativePathRenderer.mViewportHeight = viewportHeight;
+ }
+
+ @LayoutlibDelegate
+ static boolean nSetRootAlpha(long rendererPtr, float alpha) {
+ VPathRenderer_Delegate nativePathRenderer = VNativeObject.getDelegate(rendererPtr);
+ nativePathRenderer.setRootAlpha(alpha);
+
+ return true;
+ }
+
+ @LayoutlibDelegate
+ static float nGetRootAlpha(long rendererPtr) {
+ VPathRenderer_Delegate nativePathRenderer = VNativeObject.getDelegate(rendererPtr);
+
+ return nativePathRenderer.getRootAlpha();
+ }
+
+ @LayoutlibDelegate
+ static void nSetAllowCaching(long rendererPtr, boolean allowCaching) {
+ // ignored
+ }
+
+ @LayoutlibDelegate
+ static int nDraw(long rendererPtr, long canvasWrapperPtr,
+ long colorFilterPtr, Rect bounds, boolean needsMirroring, boolean canReuseCache) {
+ VPathRenderer_Delegate nativePathRenderer = VNativeObject.getDelegate(rendererPtr);
+
+ Canvas_Delegate.nSave(canvasWrapperPtr, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
+ Canvas_Delegate.nTranslate(canvasWrapperPtr, bounds.left, bounds.top);
+
+ if (needsMirroring) {
+ Canvas_Delegate.nTranslate(canvasWrapperPtr, bounds.width(), 0);
+ Canvas_Delegate.nScale(canvasWrapperPtr, -1.0f, 1.0f);
+ }
+
+ // At this point, canvas has been translated to the right position.
+ // And we use this bound for the destination rect for the drawBitmap, so
+ // we offset to (0, 0);
+ bounds.offsetTo(0, 0);
+ nativePathRenderer.draw(canvasWrapperPtr, colorFilterPtr, bounds.width(), bounds.height());
+
+ Canvas_Delegate.nRestore(canvasWrapperPtr);
+
+ return bounds.width() * bounds.height();
+ }
+
+ @LayoutlibDelegate
+ static long nCreateFullPath() {
+ return addNativeObject(new VFullPath_Delegate());
+ }
+
+ @LayoutlibDelegate
+ static long nCreateFullPath(long nativeFullPathPtr) {
+ VFullPath_Delegate original = VNativeObject.getDelegate(nativeFullPathPtr);
+ return addNativeObject(new VFullPath_Delegate(original));
+ }
+
+ @LayoutlibDelegate
+ static boolean nGetFullPathProperties(long pathPtr, byte[] propertiesData,
+ int length) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+
+ ByteBuffer properties = ByteBuffer.wrap(propertiesData);
+ properties.order(ByteOrder.nativeOrder());
+
+ properties.putFloat(VFullPath_Delegate.STROKE_WIDTH_INDEX * 4, path.getStrokeWidth());
+ properties.putInt(VFullPath_Delegate.STROKE_COLOR_INDEX * 4, path.getStrokeColor());
+ properties.putFloat(VFullPath_Delegate.STROKE_ALPHA_INDEX * 4, path.getStrokeAlpha());
+ properties.putInt(VFullPath_Delegate.FILL_COLOR_INDEX * 4, path.getFillColor());
+ properties.putFloat(VFullPath_Delegate.FILL_ALPHA_INDEX * 4, path.getStrokeAlpha());
+ properties.putFloat(VFullPath_Delegate.TRIM_PATH_START_INDEX * 4, path.getTrimPathStart());
+ properties.putFloat(VFullPath_Delegate.TRIM_PATH_END_INDEX * 4, path.getTrimPathEnd());
+ properties.putFloat(VFullPath_Delegate.TRIM_PATH_OFFSET_INDEX * 4,
+ path.getTrimPathOffset());
+ properties.putInt(VFullPath_Delegate.STROKE_LINE_CAP_INDEX * 4, path.getStrokeLineCap());
+ properties.putInt(VFullPath_Delegate.STROKE_LINE_JOIN_INDEX * 4, path.getStrokeLineJoin());
+ properties.putFloat(VFullPath_Delegate.STROKE_MITER_LIMIT_INDEX * 4,
+ path.getStrokeMiterlimit());
+ properties.putInt(VFullPath_Delegate.FILL_TYPE_INDEX * 4, path.getFillType());
+
+ return true;
+ }
+
+ @LayoutlibDelegate
+ static void nUpdateFullPathProperties(long pathPtr, float strokeWidth,
+ int strokeColor, float strokeAlpha, int fillColor, float fillAlpha, float trimPathStart,
+ float trimPathEnd, float trimPathOffset, float strokeMiterLimit, int strokeLineCap,
+ int strokeLineJoin, int fillType) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+
+ path.setStrokeWidth(strokeWidth);
+ path.setStrokeColor(strokeColor);
+ path.setStrokeAlpha(strokeAlpha);
+ path.setFillColor(fillColor);
+ path.setFillAlpha(fillAlpha);
+ path.setTrimPathStart(trimPathStart);
+ path.setTrimPathEnd(trimPathEnd);
+ path.setTrimPathOffset(trimPathOffset);
+ path.setStrokeMiterlimit(strokeMiterLimit);
+ path.setStrokeLineCap(strokeLineCap);
+ path.setStrokeLineJoin(strokeLineJoin);
+ path.setFillType(fillType);
+ }
+
+ @LayoutlibDelegate
+ static void nUpdateFullPathFillGradient(long pathPtr, long fillGradientPtr) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+
+ path.setFillGradient(fillGradientPtr);
+ }
+
+ @LayoutlibDelegate
+ static void nUpdateFullPathStrokeGradient(long pathPtr, long strokeGradientPtr) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+
+ path.setStrokeGradient(strokeGradientPtr);
+ }
+
+ @LayoutlibDelegate
+ static long nCreateClipPath() {
+ return addNativeObject(new VClipPath_Delegate());
+ }
+
+ @LayoutlibDelegate
+ static long nCreateClipPath(long clipPathPtr) {
+ VClipPath_Delegate original = VNativeObject.getDelegate(clipPathPtr);
+ return addNativeObject(new VClipPath_Delegate(original));
+ }
+
+ @LayoutlibDelegate
+ static long nCreateGroup() {
+ return addNativeObject(new VGroup_Delegate());
+ }
+
+ @LayoutlibDelegate
+ static long nCreateGroup(long groupPtr) {
+ VGroup_Delegate original = VNativeObject.getDelegate(groupPtr);
+ return addNativeObject(new VGroup_Delegate(original, new ArrayMap<>()));
+ }
+
+ @LayoutlibDelegate
+ static void nSetName(long nodePtr, String name) {
+ VNativeObject group = VNativeObject.getDelegate(nodePtr);
+ group.setName(name);
+ }
+
+ @LayoutlibDelegate
+ static boolean nGetGroupProperties(long groupPtr, float[] propertiesData,
+ int length) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+
+ FloatBuffer properties = FloatBuffer.wrap(propertiesData);
+
+ properties.put(VGroup_Delegate.ROTATE_INDEX, group.getRotation());
+ properties.put(VGroup_Delegate.PIVOT_X_INDEX, group.getPivotX());
+ properties.put(VGroup_Delegate.PIVOT_Y_INDEX, group.getPivotY());
+ properties.put(VGroup_Delegate.SCALE_X_INDEX, group.getScaleX());
+ properties.put(VGroup_Delegate.SCALE_Y_INDEX, group.getScaleY());
+ properties.put(VGroup_Delegate.TRANSLATE_X_INDEX, group.getTranslateX());
+ properties.put(VGroup_Delegate.TRANSLATE_Y_INDEX, group.getTranslateY());
+
+ return true;
+ }
+ @LayoutlibDelegate
+ static void nUpdateGroupProperties(long groupPtr, float rotate, float pivotX,
+ float pivotY, float scaleX, float scaleY, float translateX, float translateY) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+
+ group.setRotation(rotate);
+ group.setPivotX(pivotX);
+ group.setPivotY(pivotY);
+ group.setScaleX(scaleX);
+ group.setScaleY(scaleY);
+ group.setTranslateX(translateX);
+ group.setTranslateY(translateY);
+ }
+
+ @LayoutlibDelegate
+ static void nAddChild(long groupPtr, long nodePtr) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ group.mChildren.add(VNativeObject.getDelegate(nodePtr));
+ }
+
+ @LayoutlibDelegate
+ static void nSetPathString(long pathPtr, String pathString, int length) {
+ VPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ path.setPathData(PathParser_Delegate.createNodesFromPathData(pathString));
+ }
+
+ /**
+ * The setters and getters below for paths and groups are here temporarily, and will be removed
+ * once the animation in AVD is replaced with RenderNodeAnimator, in which case the animation
+ * will modify these properties in native. By then no JNI hopping would be necessary for VD
+ * during animation, and these setters and getters will be obsolete.
+ */
+ // Setters and getters during animation.
+ @LayoutlibDelegate
+ static float nGetRotation(long groupPtr) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ return group.getRotation();
+ }
+
+ @LayoutlibDelegate
+ static void nSetRotation(long groupPtr, float rotation) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ group.setRotation(rotation);
+ }
+
+ @LayoutlibDelegate
+ static float nGetPivotX(long groupPtr) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ return group.getPivotX();
+ }
+
+ @LayoutlibDelegate
+ static void nSetPivotX(long groupPtr, float pivotX) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ group.setPivotX(pivotX);
+ }
+
+ @LayoutlibDelegate
+ static float nGetPivotY(long groupPtr) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ return group.getPivotY();
+ }
+
+ @LayoutlibDelegate
+ static void nSetPivotY(long groupPtr, float pivotY) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ group.setPivotY(pivotY);
+ }
+
+ @LayoutlibDelegate
+ static float nGetScaleX(long groupPtr) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ return group.getScaleX();
+ }
+
+ @LayoutlibDelegate
+ static void nSetScaleX(long groupPtr, float scaleX) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ group.setScaleX(scaleX);
+ }
+
+ @LayoutlibDelegate
+ static float nGetScaleY(long groupPtr) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ return group.getScaleY();
+ }
+
+ @LayoutlibDelegate
+ static void nSetScaleY(long groupPtr, float scaleY) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ group.setScaleY(scaleY);
+ }
+
+ @LayoutlibDelegate
+ static float nGetTranslateX(long groupPtr) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ return group.getTranslateX();
+ }
+
+ @LayoutlibDelegate
+ static void nSetTranslateX(long groupPtr, float translateX) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ group.setTranslateX(translateX);
+ }
+
+ @LayoutlibDelegate
+ static float nGetTranslateY(long groupPtr) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ return group.getTranslateY();
+ }
+
+ @LayoutlibDelegate
+ static void nSetTranslateY(long groupPtr, float translateY) {
+ VGroup_Delegate group = VNativeObject.getDelegate(groupPtr);
+ group.setTranslateY(translateY);
+ }
+
+ @LayoutlibDelegate
+ static void nSetPathData(long pathPtr, long pathDataPtr) {
+ VPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ path.setPathData(PathParser_Delegate.getDelegate(pathDataPtr).getPathDataNodes());
+ }
+
+ @LayoutlibDelegate
+ static float nGetStrokeWidth(long pathPtr) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ return path.getStrokeWidth();
+ }
+
+ @LayoutlibDelegate
+ static void nSetStrokeWidth(long pathPtr, float width) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ path.setStrokeWidth(width);
+ }
+
+ @LayoutlibDelegate
+ static int nGetStrokeColor(long pathPtr) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ return path.getStrokeColor();
+ }
+
+ @LayoutlibDelegate
+ static void nSetStrokeColor(long pathPtr, int strokeColor) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ path.setStrokeColor(strokeColor);
+ }
+
+ @LayoutlibDelegate
+ static float nGetStrokeAlpha(long pathPtr) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ return path.getStrokeAlpha();
+ }
+
+ @LayoutlibDelegate
+ static void nSetStrokeAlpha(long pathPtr, float alpha) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ path.setStrokeAlpha(alpha);
+ }
+
+ @LayoutlibDelegate
+ static int nGetFillColor(long pathPtr) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ return path.getFillColor();
+ }
+
+ @LayoutlibDelegate
+ static void nSetFillColor(long pathPtr, int fillColor) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ path.setFillColor(fillColor);
+ }
+
+ @LayoutlibDelegate
+ static float nGetFillAlpha(long pathPtr) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ return path.getFillAlpha();
+ }
+
+ @LayoutlibDelegate
+ static void nSetFillAlpha(long pathPtr, float fillAlpha) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ path.setFillAlpha(fillAlpha);
+ }
+
+ @LayoutlibDelegate
+ static float nGetTrimPathStart(long pathPtr) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ return path.getTrimPathStart();
+ }
+
+ @LayoutlibDelegate
+ static void nSetTrimPathStart(long pathPtr, float trimPathStart) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ path.setTrimPathStart(trimPathStart);
+ }
+
+ @LayoutlibDelegate
+ static float nGetTrimPathEnd(long pathPtr) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ return path.getTrimPathEnd();
+ }
+
+ @LayoutlibDelegate
+ static void nSetTrimPathEnd(long pathPtr, float trimPathEnd) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ path.setTrimPathEnd(trimPathEnd);
+ }
+
+ @LayoutlibDelegate
+ static float nGetTrimPathOffset(long pathPtr) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ return path.getTrimPathOffset();
+ }
+
+ @LayoutlibDelegate
+ static void nSetTrimPathOffset(long pathPtr, float trimPathOffset) {
+ VFullPath_Delegate path = VNativeObject.getDelegate(pathPtr);
+ path.setTrimPathOffset(trimPathOffset);
+ }
+
+ /**
+ * Base class for all the internal Delegates that does two functions:
+ * <ol>
+ * <li>Serves as base class to store all the delegates in one {@link DelegateManager}
+ * <li>Provides setName for all the classes. {@link VPathRenderer_Delegate} does actually
+ * not need it
+ * </ol>
+ */
+ abstract static class VNativeObject {
+ long mNativePtr = 0;
+
+ @NonNull
+ static <T> T getDelegate(long nativePtr) {
+ //noinspection unchecked
+ T vNativeObject = (T) sPathManager.getDelegate(nativePtr);
+
+ assert vNativeObject != null;
+ return vNativeObject;
+ }
+
+ abstract void setName(String name);
+
+ void setNativePtr(long nativePtr) {
+ mNativePtr = nativePtr;
+ }
+
+ /**
+ * Method to explicitly dispose native objects
+ */
+ void dispose() {
+ }
+ }
+
+ private static class VClipPath_Delegate extends VPath_Delegate {
+ private VClipPath_Delegate() {
+ // Empty constructor.
+ }
+
+ private VClipPath_Delegate(VClipPath_Delegate copy) {
+ super(copy);
+ }
+
+ @Override
+ public boolean isClipPath() {
+ return true;
+ }
+ }
+
+ static class VFullPath_Delegate extends VPath_Delegate {
+ // These constants need to be kept in sync with their values in VectorDrawable.VFullPath
+ private static final int STROKE_WIDTH_INDEX = 0;
+ private static final int STROKE_COLOR_INDEX = 1;
+ private static final int STROKE_ALPHA_INDEX = 2;
+ private static final int FILL_COLOR_INDEX = 3;
+ private static final int FILL_ALPHA_INDEX = 4;
+ private static final int TRIM_PATH_START_INDEX = 5;
+ private static final int TRIM_PATH_END_INDEX = 6;
+ private static final int TRIM_PATH_OFFSET_INDEX = 7;
+ private static final int STROKE_LINE_CAP_INDEX = 8;
+ private static final int STROKE_LINE_JOIN_INDEX = 9;
+ private static final int STROKE_MITER_LIMIT_INDEX = 10;
+ private static final int FILL_TYPE_INDEX = 11;
+
+ private static final int LINECAP_BUTT = 0;
+ private static final int LINECAP_ROUND = 1;
+ private static final int LINECAP_SQUARE = 2;
+
+ private static final int LINEJOIN_MITER = 0;
+ private static final int LINEJOIN_ROUND = 1;
+ private static final int LINEJOIN_BEVEL = 2;
+
+ @NonNull
+ public Consumer<Float> getFloatPropertySetter(int propertyIdx) {
+ switch (propertyIdx) {
+ case STROKE_WIDTH_INDEX:
+ return this::setStrokeWidth;
+ case STROKE_ALPHA_INDEX:
+ return this::setStrokeAlpha;
+ case FILL_ALPHA_INDEX:
+ return this::setFillAlpha;
+ case TRIM_PATH_START_INDEX:
+ return this::setTrimPathStart;
+ case TRIM_PATH_END_INDEX:
+ return this::setTrimPathEnd;
+ case TRIM_PATH_OFFSET_INDEX:
+ return this::setTrimPathOffset;
+ }
+
+ assert false : ("Invalid VFullPath_Delegate property index " + propertyIdx);
+ return t -> {};
+ }
+
+ @NonNull
+ public Consumer<Integer> getIntPropertySetter(int propertyIdx) {
+ switch (propertyIdx) {
+ case STROKE_COLOR_INDEX:
+ return this::setStrokeColor;
+ case FILL_COLOR_INDEX:
+ return this::setFillColor;
+ }
+
+ assert false : ("Invalid VFullPath_Delegate property index " + propertyIdx);
+ return t -> {};
+ }
+
+ /////////////////////////////////////////////////////
+ // Variables below need to be copied (deep copy if applicable) for mutation.
+
+ int mStrokeColor = Color.TRANSPARENT;
+ float mStrokeWidth = 0;
+
+ int mFillColor = Color.TRANSPARENT;
+ long mStrokeGradient = 0;
+ long mFillGradient = 0;
+ float mStrokeAlpha = 1.0f;
+ float mFillAlpha = 1.0f;
+ float mTrimPathStart = 0;
+ float mTrimPathEnd = 1;
+ float mTrimPathOffset = 0;
+
+ Cap mStrokeLineCap = BUTT;
+ Join mStrokeLineJoin = MITER;
+ float mStrokeMiterlimit = 4;
+
+ int mFillType = 0; // WINDING(0) is the default value. See Path.FillType
+
+ private VFullPath_Delegate() {
+ // Empty constructor.
+ }
+
+ private VFullPath_Delegate(VFullPath_Delegate copy) {
+ super(copy);
+
+ mStrokeColor = copy.mStrokeColor;
+ mStrokeWidth = copy.mStrokeWidth;
+ mStrokeAlpha = copy.mStrokeAlpha;
+ mFillColor = copy.mFillColor;
+ mFillAlpha = copy.mFillAlpha;
+ mTrimPathStart = copy.mTrimPathStart;
+ mTrimPathEnd = copy.mTrimPathEnd;
+ mTrimPathOffset = copy.mTrimPathOffset;
+
+ mStrokeLineCap = copy.mStrokeLineCap;
+ mStrokeLineJoin = copy.mStrokeLineJoin;
+ mStrokeMiterlimit = copy.mStrokeMiterlimit;
+
+ mStrokeGradient = copy.mStrokeGradient;
+ mFillGradient = copy.mFillGradient;
+ mFillType = copy.mFillType;
+ }
+
+ private int getStrokeLineCap() {
+ switch (mStrokeLineCap) {
+ case BUTT:
+ return LINECAP_BUTT;
+ case ROUND:
+ return LINECAP_ROUND;
+ case SQUARE:
+ return LINECAP_SQUARE;
+ default:
+ assert false;
+ }
+
+ return -1;
+ }
+
+ private void setStrokeLineCap(int cap) {
+ switch (cap) {
+ case LINECAP_BUTT:
+ mStrokeLineCap = BUTT;
+ break;
+ case LINECAP_ROUND:
+ mStrokeLineCap = ROUND;
+ break;
+ case LINECAP_SQUARE:
+ mStrokeLineCap = SQUARE;
+ break;
+ default:
+ assert false;
+ }
+ }
+
+ private int getStrokeLineJoin() {
+ switch (mStrokeLineJoin) {
+ case MITER:
+ return LINEJOIN_MITER;
+ case ROUND:
+ return LINEJOIN_ROUND;
+ case BEVEL:
+ return LINEJOIN_BEVEL;
+ default:
+ assert false;
+ }
+
+ return -1;
+ }
+
+ private void setStrokeLineJoin(int join) {
+ switch (join) {
+ case LINEJOIN_BEVEL:
+ mStrokeLineJoin = BEVEL;
+ break;
+ case LINEJOIN_MITER:
+ mStrokeLineJoin = MITER;
+ break;
+ case LINEJOIN_ROUND:
+ mStrokeLineJoin = Join.ROUND;
+ break;
+ default:
+ assert false;
+ }
+ }
+
+ private int getStrokeColor() {
+ return mStrokeColor;
+ }
+
+ private void setStrokeColor(int strokeColor) {
+ mStrokeColor = strokeColor;
+ }
+
+ private float getStrokeWidth() {
+ return mStrokeWidth;
+ }
+
+ private void setStrokeWidth(float strokeWidth) {
+ mStrokeWidth = strokeWidth;
+ }
+
+ private float getStrokeAlpha() {
+ return mStrokeAlpha;
+ }
+
+ private void setStrokeAlpha(float strokeAlpha) {
+ mStrokeAlpha = strokeAlpha;
+ }
+
+ private int getFillColor() {
+ return mFillColor;
+ }
+
+ private void setFillColor(int fillColor) {
+ mFillColor = fillColor;
+ }
+
+ private float getFillAlpha() {
+ return mFillAlpha;
+ }
+
+ private void setFillAlpha(float fillAlpha) {
+ mFillAlpha = fillAlpha;
+ }
+
+ private float getTrimPathStart() {
+ return mTrimPathStart;
+ }
+
+ private void setTrimPathStart(float trimPathStart) {
+ mTrimPathStart = trimPathStart;
+ }
+
+ private float getTrimPathEnd() {
+ return mTrimPathEnd;
+ }
+
+ private void setTrimPathEnd(float trimPathEnd) {
+ mTrimPathEnd = trimPathEnd;
+ }
+
+ private float getTrimPathOffset() {
+ return mTrimPathOffset;
+ }
+
+ private void setTrimPathOffset(float trimPathOffset) {
+ mTrimPathOffset = trimPathOffset;
+ }
+
+ private void setStrokeMiterlimit(float limit) {
+ mStrokeMiterlimit = limit;
+ }
+
+ private float getStrokeMiterlimit() {
+ return mStrokeMiterlimit;
+ }
+
+ private void setStrokeGradient(long gradientPtr) {
+ mStrokeGradient = gradientPtr;
+ }
+
+ private void setFillGradient(long gradientPtr) {
+ mFillGradient = gradientPtr;
+ }
+
+ private void setFillType(int fillType) {
+ mFillType = fillType;
+ }
+
+ private int getFillType() {
+ return mFillType;
+ }
+ }
+
+ static class VGroup_Delegate extends VNativeObject {
+ // This constants need to be kept in sync with their definitions in VectorDrawable.Group
+ private static final int ROTATE_INDEX = 0;
+ private static final int PIVOT_X_INDEX = 1;
+ private static final int PIVOT_Y_INDEX = 2;
+ private static final int SCALE_X_INDEX = 3;
+ private static final int SCALE_Y_INDEX = 4;
+ private static final int TRANSLATE_X_INDEX = 5;
+ private static final int TRANSLATE_Y_INDEX = 6;
+
+ public Consumer<Float> getPropertySetter(int propertyIdx) {
+ switch (propertyIdx) {
+ case ROTATE_INDEX:
+ return this::setRotation;
+ case PIVOT_X_INDEX:
+ return this::setPivotX;
+ case PIVOT_Y_INDEX:
+ return this::setPivotY;
+ case SCALE_X_INDEX:
+ return this::setScaleX;
+ case SCALE_Y_INDEX:
+ return this::setScaleY;
+ case TRANSLATE_X_INDEX:
+ return this::setTranslateX;
+ case TRANSLATE_Y_INDEX:
+ return this::setTranslateY;
+ }
+
+ assert false : ("Invalid VGroup_Delegate property index " + propertyIdx);
+ return t -> {};
+ }
+
+ /////////////////////////////////////////////////////
+ // Variables below need to be copied (deep copy if applicable) for mutation.
+ final ArrayList<Object> mChildren = new ArrayList<>();
+ // mStackedMatrix is only used temporarily when drawing, it combines all
+ // the parents' local matrices with the current one.
+ private final Matrix mStackedMatrix = new Matrix();
+ // mLocalMatrix is updated based on the update of transformation information,
+ // either parsed from the XML or by animation.
+ private final Matrix mLocalMatrix = new Matrix();
+ private float mRotate = 0;
+ private float mPivotX = 0;
+ private float mPivotY = 0;
+ private float mScaleX = 1;
+ private float mScaleY = 1;
+ private float mTranslateX = 0;
+ private float mTranslateY = 0;
+ private int mChangingConfigurations;
+ private String mGroupName = null;
+
+ private VGroup_Delegate(VGroup_Delegate copy, ArrayMap<String, Object> targetsMap) {
+ mRotate = copy.mRotate;
+ mPivotX = copy.mPivotX;
+ mPivotY = copy.mPivotY;
+ mScaleX = copy.mScaleX;
+ mScaleY = copy.mScaleY;
+ mTranslateX = copy.mTranslateX;
+ mTranslateY = copy.mTranslateY;
+ mGroupName = copy.mGroupName;
+ mChangingConfigurations = copy.mChangingConfigurations;
+ if (mGroupName != null) {
+ targetsMap.put(mGroupName, this);
+ }
+
+ mLocalMatrix.set(copy.mLocalMatrix);
+ }
+
+ private VGroup_Delegate() {
+ }
+
+ private void updateLocalMatrix() {
+ // The order we apply is the same as the
+ // RenderNode.cpp::applyViewPropertyTransforms().
+ mLocalMatrix.reset();
+ mLocalMatrix.postTranslate(-mPivotX, -mPivotY);
+ mLocalMatrix.postScale(mScaleX, mScaleY);
+ mLocalMatrix.postRotate(mRotate, 0, 0);
+ mLocalMatrix.postTranslate(mTranslateX + mPivotX, mTranslateY + mPivotY);
+ }
+
+ /* Setters and Getters, used by animator from AnimatedVectorDrawable. */
+ private float getRotation() {
+ return mRotate;
+ }
+
+ private void setRotation(float rotation) {
+ if (rotation != mRotate) {
+ mRotate = rotation;
+ updateLocalMatrix();
+ }
+ }
+
+ private float getPivotX() {
+ return mPivotX;
+ }
+
+ private void setPivotX(float pivotX) {
+ if (pivotX != mPivotX) {
+ mPivotX = pivotX;
+ updateLocalMatrix();
+ }
+ }
+
+ private float getPivotY() {
+ return mPivotY;
+ }
+
+ private void setPivotY(float pivotY) {
+ if (pivotY != mPivotY) {
+ mPivotY = pivotY;
+ updateLocalMatrix();
+ }
+ }
+
+ private float getScaleX() {
+ return mScaleX;
+ }
+
+ private void setScaleX(float scaleX) {
+ if (scaleX != mScaleX) {
+ mScaleX = scaleX;
+ updateLocalMatrix();
+ }
+ }
+
+ private float getScaleY() {
+ return mScaleY;
+ }
+
+ private void setScaleY(float scaleY) {
+ if (scaleY != mScaleY) {
+ mScaleY = scaleY;
+ updateLocalMatrix();
+ }
+ }
+
+ private float getTranslateX() {
+ return mTranslateX;
+ }
+
+ private void setTranslateX(float translateX) {
+ if (translateX != mTranslateX) {
+ mTranslateX = translateX;
+ updateLocalMatrix();
+ }
+ }
+
+ private float getTranslateY() {
+ return mTranslateY;
+ }
+
+ private void setTranslateY(float translateY) {
+ if (translateY != mTranslateY) {
+ mTranslateY = translateY;
+ updateLocalMatrix();
+ }
+ }
+
+ @Override
+ public void setName(String name) {
+ mGroupName = name;
+ }
+
+ @Override
+ protected void dispose() {
+ mChildren.stream().filter(child -> child instanceof VNativeObject).forEach(child
+ -> {
+ VNativeObject nativeObject = (VNativeObject) child;
+ if (nativeObject.mNativePtr != 0) {
+ sPathManager.removeJavaReferenceFor(nativeObject.mNativePtr);
+ nativeObject.mNativePtr = 0;
+ }
+ nativeObject.dispose();
+ });
+ mChildren.clear();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ }
+ }
+
+ public static class VPath_Delegate extends VNativeObject {
+ protected PathParser_Delegate.PathDataNode[] mNodes = null;
+ String mPathName;
+ int mChangingConfigurations;
+
+ public VPath_Delegate() {
+ // Empty constructor.
+ }
+
+ public VPath_Delegate(VPath_Delegate copy) {
+ mPathName = copy.mPathName;
+ mChangingConfigurations = copy.mChangingConfigurations;
+ mNodes = copy.mNodes != null ? PathParser_Delegate.deepCopyNodes(copy.mNodes) : null;
+ }
+
+ public void toPath(Path path) {
+ path.reset();
+ if (mNodes != null) {
+ PathParser_Delegate.PathDataNode.nodesToPath(mNodes,
+ Path_Delegate.getDelegate(path.mNativePath));
+ }
+ }
+
+ @Override
+ public void setName(String name) {
+ mPathName = name;
+ }
+
+ public boolean isClipPath() {
+ return false;
+ }
+
+ private void setPathData(PathParser_Delegate.PathDataNode[] nodes) {
+ if (!PathParser_Delegate.canMorph(mNodes, nodes)) {
+ // This should not happen in the middle of animation.
+ mNodes = PathParser_Delegate.deepCopyNodes(nodes);
+ } else {
+ PathParser_Delegate.updateNodes(mNodes, nodes);
+ }
+ }
+
+ @Override
+ void dispose() {
+ mNodes = null;
+ }
+ }
+
+ static class VPathRenderer_Delegate extends VNativeObject {
+ /* Right now the internal data structure is organized as a tree.
+ * Each node can be a group node, or a path.
+ * A group node can have groups or paths as children, but a path node has
+ * no children.
+ * One example can be:
+ * Root Group
+ * / | \
+ * Group Path Group
+ * / \ |
+ * Path Path Path
+ *
+ */
+ // Variables that only used temporarily inside the draw() call, so there
+ // is no need for deep copying.
+ private final Path mPath;
+ private final Path mRenderPath;
+ private final Matrix mFinalPathMatrix = new Matrix();
+ private final long mRootGroupPtr;
+ private float mViewportWidth = 0;
+ private float mViewportHeight = 0;
+ private float mRootAlpha = 1.0f;
+ private Paint mStrokePaint;
+ private Paint mFillPaint;
+ private PathMeasure mPathMeasure;
+
+ private VPathRenderer_Delegate(long rootGroupPtr) {
+ mRootGroupPtr = rootGroupPtr;
+ mPath = new Path();
+ mRenderPath = new Path();
+ }
+
+ private VPathRenderer_Delegate(VPathRenderer_Delegate rendererToCopy,
+ long rootGroupPtr) {
+ this(rootGroupPtr);
+ mViewportWidth = rendererToCopy.mViewportWidth;
+ mViewportHeight = rendererToCopy.mViewportHeight;
+ mRootAlpha = rendererToCopy.mRootAlpha;
+ }
+
+ private float getRootAlpha() {
+ return mRootAlpha;
+ }
+
+ void setRootAlpha(float alpha) {
+ mRootAlpha = alpha;
+ }
+
+ private void drawGroupTree(VGroup_Delegate currentGroup, Matrix currentMatrix,
+ long canvasPtr, int w, int h, long filterPtr) {
+ // Calculate current group's matrix by preConcat the parent's and
+ // and the current one on the top of the stack.
+ // Basically the Mfinal = Mviewport * M0 * M1 * M2;
+ // Mi the local matrix at level i of the group tree.
+ currentGroup.mStackedMatrix.set(currentMatrix);
+ currentGroup.mStackedMatrix.preConcat(currentGroup.mLocalMatrix);
+
+ // Save the current clip information, which is local to this group.
+ Canvas_Delegate.nSave(canvasPtr, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
+ // Draw the group tree in the same order as the XML file.
+ for (int i = 0; i < currentGroup.mChildren.size(); i++) {
+ Object child = currentGroup.mChildren.get(i);
+ if (child instanceof VGroup_Delegate) {
+ VGroup_Delegate childGroup = (VGroup_Delegate) child;
+ drawGroupTree(childGroup, currentGroup.mStackedMatrix,
+ canvasPtr, w, h, filterPtr);
+ } else if (child instanceof VPath_Delegate) {
+ VPath_Delegate childPath = (VPath_Delegate) child;
+ drawPath(currentGroup, childPath, canvasPtr, w, h, filterPtr);
+ }
+ }
+ Canvas_Delegate.nRestore(canvasPtr);
+ }
+
+ public void draw(long canvasPtr, long filterPtr, int w, int h) {
+ // Traverse the tree in pre-order to draw.
+ drawGroupTree(VNativeObject.getDelegate(mRootGroupPtr), Matrix.IDENTITY_MATRIX, canvasPtr, w, h, filterPtr);
+ }
+
+ private void drawPath(VGroup_Delegate VGroup, VPath_Delegate VPath, long canvasPtr,
+ int w,
+ int h,
+ long filterPtr) {
+ final float scaleX = w / mViewportWidth;
+ final float scaleY = h / mViewportHeight;
+ final float minScale = Math.min(scaleX, scaleY);
+ final Matrix groupStackedMatrix = VGroup.mStackedMatrix;
+
+ mFinalPathMatrix.set(groupStackedMatrix);
+ mFinalPathMatrix.postScale(scaleX, scaleY);
+
+ final float matrixScale = getMatrixScale(groupStackedMatrix);
+ if (matrixScale == 0) {
+ // When either x or y is scaled to 0, we don't need to draw anything.
+ return;
+ }
+ VPath.toPath(mPath);
+ final Path path = mPath;
+
+ mRenderPath.reset();
+
+ if (VPath.isClipPath()) {
+ mRenderPath.addPath(path, mFinalPathMatrix);
+ Canvas_Delegate.nClipPath(canvasPtr, mRenderPath.mNativePath, Op
+ .INTERSECT.nativeInt);
+ } else {
+ VFullPath_Delegate fullPath = (VFullPath_Delegate) VPath;
+ if (fullPath.mTrimPathStart != 0.0f || fullPath.mTrimPathEnd != 1.0f) {
+ float start = (fullPath.mTrimPathStart + fullPath.mTrimPathOffset) % 1.0f;
+ float end = (fullPath.mTrimPathEnd + fullPath.mTrimPathOffset) % 1.0f;
+
+ if (mPathMeasure == null) {
+ mPathMeasure = new PathMeasure();
+ }
+ mPathMeasure.setPath(mPath, false);
+
+ float len = mPathMeasure.getLength();
+ start = start * len;
+ end = end * len;
+ path.reset();
+ if (start > end) {
+ mPathMeasure.getSegment(start, len, path, true);
+ mPathMeasure.getSegment(0f, end, path, true);
+ } else {
+ mPathMeasure.getSegment(start, end, path, true);
+ }
+ path.rLineTo(0, 0); // fix bug in measure
+ }
+ mRenderPath.addPath(path, mFinalPathMatrix);
+
+ if (fullPath.mFillColor != Color.TRANSPARENT) {
+ if (mFillPaint == null) {
+ mFillPaint = new Paint();
+ mFillPaint.setStyle(Style.FILL);
+ mFillPaint.setAntiAlias(true);
+ }
+
+ final Paint fillPaint = mFillPaint;
+ fillPaint.setColor(applyAlpha(applyAlpha(fullPath.mFillColor, fullPath
+ .mFillAlpha), getRootAlpha()));
+ Paint_Delegate fillPaintDelegate = Paint_Delegate.getDelegate(fillPaint
+ .getNativeInstance());
+ // mFillPaint can not be null at this point so we will have a delegate
+ assert fillPaintDelegate != null;
+ fillPaintDelegate.setColorFilter(filterPtr);
+
+ Shader_Delegate shaderDelegate =
+ Shader_Delegate.getDelegate(fullPath.mFillGradient);
+ if (shaderDelegate != null) {
+ // If there is a shader, apply the local transformation to make sure
+ // the gradient is transformed to match the viewport
+ shaderDelegate.setLocalMatrix(mFinalPathMatrix.native_instance);
+ }
+
+ fillPaintDelegate.setShader(fullPath.mFillGradient);
+ Path_Delegate.nSetFillType(mRenderPath.mNativePath, fullPath.mFillType);
+ BaseCanvas_Delegate.nDrawPath(canvasPtr, mRenderPath.mNativePath, fillPaint
+ .getNativeInstance());
+ if (shaderDelegate != null) {
+ // Remove the local matrix
+ shaderDelegate.setLocalMatrix(0);
+ }
+ }
+
+ if (fullPath.mStrokeColor != Color.TRANSPARENT) {
+ if (mStrokePaint == null) {
+ mStrokePaint = new Paint();
+ mStrokePaint.setStyle(Style.STROKE);
+ mStrokePaint.setAntiAlias(true);
+ }
+
+ final Paint strokePaint = mStrokePaint;
+ if (fullPath.mStrokeLineJoin != null) {
+ strokePaint.setStrokeJoin(fullPath.mStrokeLineJoin);
+ }
+
+ if (fullPath.mStrokeLineCap != null) {
+ strokePaint.setStrokeCap(fullPath.mStrokeLineCap);
+ }
+
+ strokePaint.setStrokeMiter(fullPath.mStrokeMiterlimit);
+ strokePaint.setColor(applyAlpha(applyAlpha(fullPath.mStrokeColor, fullPath
+ .mStrokeAlpha), getRootAlpha()));
+ Paint_Delegate strokePaintDelegate = Paint_Delegate.getDelegate(strokePaint
+ .getNativeInstance());
+ // mStrokePaint can not be null at this point so we will have a delegate
+ assert strokePaintDelegate != null;
+ strokePaintDelegate.setColorFilter(filterPtr);
+ final float finalStrokeScale = minScale * matrixScale;
+ strokePaint.setStrokeWidth(fullPath.mStrokeWidth * finalStrokeScale);
+ strokePaintDelegate.setShader(fullPath.mStrokeGradient);
+ BaseCanvas_Delegate.nDrawPath(canvasPtr, mRenderPath.mNativePath, strokePaint
+ .getNativeInstance());
+ }
+ }
+ }
+
+ private float getMatrixScale(Matrix groupStackedMatrix) {
+ // Given unit vectors A = (0, 1) and B = (1, 0).
+ // After matrix mapping, we got A' and B'. Let theta = the angel b/t A' and B'.
+ // Therefore, the final scale we want is min(|A'| * sin(theta), |B'| * sin(theta)),
+ // which is (|A'| * |B'| * sin(theta)) / max (|A'|, |B'|);
+ // If max (|A'|, |B'|) = 0, that means either x or y has a scale of 0.
+ //
+ // For non-skew case, which is most of the cases, matrix scale is computing exactly the
+ // scale on x and y axis, and take the minimal of these two.
+ // For skew case, an unit square will mapped to a parallelogram. And this function will
+ // return the minimal height of the 2 bases.
+ float[] unitVectors = new float[]{0, 1, 1, 0};
+ groupStackedMatrix.mapVectors(unitVectors);
+ float scaleX = MathUtils.mag(unitVectors[0], unitVectors[1]);
+ float scaleY = MathUtils.mag(unitVectors[2], unitVectors[3]);
+ float crossProduct = MathUtils.cross(unitVectors[0], unitVectors[1],
+ unitVectors[2], unitVectors[3]);
+ float maxScale = MathUtils.max(scaleX, scaleY);
+
+ float matrixScale = 0;
+ if (maxScale > 0) {
+ matrixScale = MathUtils.abs(crossProduct) / maxScale;
+ }
+ if (DBG_VECTOR_DRAWABLE) {
+ Log.d(LOGTAG, "Scale x " + scaleX + " y " + scaleY + " final " + matrixScale);
+ }
+ return matrixScale;
+ }
+
+ @Override
+ public void setName(String name) {
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ // The mRootGroupPtr is not explicitly freed by anything in the VectorDrawable so we
+ // need to free it here.
+ VNativeObject nativeObject = sPathManager.getDelegate(mRootGroupPtr);
+ sPathManager.removeJavaReferenceFor(mRootGroupPtr);
+ assert nativeObject != null;
+ nativeObject.dispose();
+
+ super.finalize();
+ }
+ }
+}
diff --git a/android/graphics/drawable/shapes/ArcShape.java b/android/graphics/drawable/shapes/ArcShape.java
new file mode 100644
index 0000000..85ba0a9
--- /dev/null
+++ b/android/graphics/drawable/shapes/ArcShape.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007 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 android.graphics.drawable.shapes;
+
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Paint;
+
+/**
+ * Creates an arc shape. The arc shape starts at a specified angle and sweeps
+ * clockwise, drawing slices of pie.
+ * <p>
+ * The arc can be drawn to a {@link Canvas} with its own
+ * {@link #draw(Canvas, Paint)} method, but more graphical control is available
+ * if you instead pass the ArcShape to a
+ * {@link android.graphics.drawable.ShapeDrawable}.
+ */
+public class ArcShape extends RectShape {
+ private final float mStartAngle;
+ private final float mSweepAngle;
+
+ /**
+ * ArcShape constructor.
+ *
+ * @param startAngle the angle (in degrees) where the arc begins
+ * @param sweepAngle the sweep angle (in degrees). Anything equal to or
+ * greater than 360 results in a complete circle/oval.
+ */
+ public ArcShape(float startAngle, float sweepAngle) {
+ mStartAngle = startAngle;
+ mSweepAngle = sweepAngle;
+ }
+
+ /**
+ * @return the angle (in degrees) where the arc begins
+ */
+ public final float getStartAngle() {
+ return mStartAngle;
+ }
+
+ /**
+ * @return the sweep angle (in degrees)
+ */
+ public final float getSweepAngle() {
+ return mSweepAngle;
+ }
+
+ @Override
+ public void draw(Canvas canvas, Paint paint) {
+ canvas.drawArc(rect(), mStartAngle, mSweepAngle, true, paint);
+ }
+
+ @Override
+ public void getOutline(Outline outline) {
+ // Since we don't support concave outlines, arc shape does not attempt
+ // to provide an outline.
+ }
+
+ @Override
+ public ArcShape clone() throws CloneNotSupportedException {
+ return (ArcShape) super.clone();
+ }
+}
+
diff --git a/android/graphics/drawable/shapes/OvalShape.java b/android/graphics/drawable/shapes/OvalShape.java
new file mode 100644
index 0000000..fb87d28
--- /dev/null
+++ b/android/graphics/drawable/shapes/OvalShape.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2007 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 android.graphics.drawable.shapes;
+
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+/**
+ * Defines an oval shape.
+ * <p>
+ * The oval can be drawn to a Canvas with its own draw() method,
+ * but more graphical control is available if you instead pass
+ * the OvalShape to a {@link android.graphics.drawable.ShapeDrawable}.
+ */
+public class OvalShape extends RectShape {
+
+ public OvalShape() {}
+
+ @Override
+ public void draw(Canvas canvas, Paint paint) {
+ canvas.drawOval(rect(), paint);
+ }
+
+ @Override
+ public void getOutline(Outline outline) {
+ final RectF rect = rect();
+ outline.setOval((int) Math.ceil(rect.left), (int) Math.ceil(rect.top),
+ (int) Math.floor(rect.right), (int) Math.floor(rect.bottom));
+ }
+
+ @Override
+ public OvalShape clone() throws CloneNotSupportedException {
+ return (OvalShape) super.clone();
+ }
+}
+
diff --git a/android/graphics/drawable/shapes/PathShape.java b/android/graphics/drawable/shapes/PathShape.java
new file mode 100644
index 0000000..ce5552b
--- /dev/null
+++ b/android/graphics/drawable/shapes/PathShape.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007 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 android.graphics.drawable.shapes;
+
+import android.annotation.NonNull;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+
+/**
+ * Creates geometric paths, utilizing the {@link android.graphics.Path} class.
+ * <p>
+ * The path can be drawn to a Canvas with its own draw() method,
+ * but more graphical control is available if you instead pass
+ * the PathShape to a {@link android.graphics.drawable.ShapeDrawable}.
+ */
+public class PathShape extends Shape {
+ private final float mStdWidth;
+ private final float mStdHeight;
+
+ private Path mPath;
+
+ private float mScaleX; // cached from onResize
+ private float mScaleY; // cached from onResize
+
+ /**
+ * PathShape constructor.
+ *
+ * @param path a Path that defines the geometric paths for this shape
+ * @param stdWidth the standard width for the shape. Any changes to the
+ * width with resize() will result in a width scaled based
+ * on the new width divided by this width.
+ * @param stdHeight the standard height for the shape. Any changes to the
+ * height with resize() will result in a height scaled based
+ * on the new height divided by this height.
+ */
+ public PathShape(@NonNull Path path, float stdWidth, float stdHeight) {
+ mPath = path;
+ mStdWidth = stdWidth;
+ mStdHeight = stdHeight;
+ }
+
+ @Override
+ public void draw(Canvas canvas, Paint paint) {
+ canvas.save();
+ canvas.scale(mScaleX, mScaleY);
+ canvas.drawPath(mPath, paint);
+ canvas.restore();
+ }
+
+ @Override
+ protected void onResize(float width, float height) {
+ mScaleX = width / mStdWidth;
+ mScaleY = height / mStdHeight;
+ }
+
+ @Override
+ public PathShape clone() throws CloneNotSupportedException {
+ final PathShape shape = (PathShape) super.clone();
+ shape.mPath = new Path(mPath);
+ return shape;
+ }
+}
+
diff --git a/android/graphics/drawable/shapes/RectShape.java b/android/graphics/drawable/shapes/RectShape.java
new file mode 100644
index 0000000..e339a21
--- /dev/null
+++ b/android/graphics/drawable/shapes/RectShape.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2007 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 android.graphics.drawable.shapes;
+
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+/**
+ * Defines a rectangle shape.
+ * <p>
+ * The rectangle can be drawn to a Canvas with its own draw() method,
+ * but more graphical control is available if you instead pass
+ * the RectShape to a {@link android.graphics.drawable.ShapeDrawable}.
+ */
+public class RectShape extends Shape {
+ private RectF mRect = new RectF();
+
+ public RectShape() {}
+
+ @Override
+ public void draw(Canvas canvas, Paint paint) {
+ canvas.drawRect(mRect, paint);
+ }
+
+ @Override
+ public void getOutline(Outline outline) {
+ final RectF rect = rect();
+ outline.setRect((int) Math.ceil(rect.left), (int) Math.ceil(rect.top),
+ (int) Math.floor(rect.right), (int) Math.floor(rect.bottom));
+ }
+
+ @Override
+ protected void onResize(float width, float height) {
+ mRect.set(0, 0, width, height);
+ }
+
+ /**
+ * Returns the RectF that defines this rectangle's bounds.
+ */
+ protected final RectF rect() {
+ return mRect;
+ }
+
+ @Override
+ public RectShape clone() throws CloneNotSupportedException {
+ final RectShape shape = (RectShape) super.clone();
+ shape.mRect = new RectF(mRect);
+ return shape;
+ }
+}
diff --git a/android/graphics/drawable/shapes/RoundRectShape.java b/android/graphics/drawable/shapes/RoundRectShape.java
new file mode 100644
index 0000000..f5cbb24
--- /dev/null
+++ b/android/graphics/drawable/shapes/RoundRectShape.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2007 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 android.graphics.drawable.shapes;
+
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+
+/**
+ * Creates a rounded-corner rectangle. Optionally, an inset (rounded) rectangle
+ * can be included (to make a sort of "O" shape).
+ * <p>
+ * The rounded rectangle can be drawn to a Canvas with its own draw() method,
+ * but more graphical control is available if you instead pass
+ * the RoundRectShape to a {@link android.graphics.drawable.ShapeDrawable}.
+ */
+public class RoundRectShape extends RectShape {
+ private float[] mOuterRadii;
+ private RectF mInset;
+ private float[] mInnerRadii;
+
+ private RectF mInnerRect;
+ private Path mPath; // this is what we actually draw
+
+ /**
+ * RoundRectShape constructor.
+ * <p>
+ * Specifies an outer (round)rect and an optional inner (round)rect.
+ *
+ * @param outerRadii An array of 8 radius values, for the outer roundrect.
+ * The first two floats are for the top-left corner
+ * (remaining pairs correspond clockwise). For no rounded
+ * corners on the outer rectangle, pass {@code null}.
+ * @param inset A RectF that specifies the distance from the inner
+ * rect to each side of the outer rect. For no inner, pass
+ * {@code null}.
+ * @param innerRadii An array of 8 radius values, for the inner roundrect.
+ * The first two floats are for the top-left corner
+ * (remaining pairs correspond clockwise). For no rounded
+ * corners on the inner rectangle, pass {@code null}. If
+ * inset parameter is {@code null}, this parameter is
+ * ignored.
+ */
+ public RoundRectShape(@Nullable float[] outerRadii, @Nullable RectF inset,
+ @Nullable float[] innerRadii) {
+ if (outerRadii != null && outerRadii.length < 8) {
+ throw new ArrayIndexOutOfBoundsException("outer radii must have >= 8 values");
+ }
+ if (innerRadii != null && innerRadii.length < 8) {
+ throw new ArrayIndexOutOfBoundsException("inner radii must have >= 8 values");
+ }
+ mOuterRadii = outerRadii;
+ mInset = inset;
+ mInnerRadii = innerRadii;
+
+ if (inset != null) {
+ mInnerRect = new RectF();
+ }
+ mPath = new Path();
+ }
+
+ @Override
+ public void draw(Canvas canvas, Paint paint) {
+ canvas.drawPath(mPath, paint);
+ }
+
+ @Override
+ public void getOutline(Outline outline) {
+ if (mInnerRect != null) return; // have a hole, can't produce valid outline
+
+ float radius = 0;
+ if (mOuterRadii != null) {
+ radius = mOuterRadii[0];
+ for (int i = 1; i < 8; i++) {
+ if (mOuterRadii[i] != radius) {
+ // can't call simple constructors, use path
+ outline.setConvexPath(mPath);
+ return;
+ }
+ }
+ }
+
+ final RectF rect = rect();
+ outline.setRoundRect((int) Math.ceil(rect.left), (int) Math.ceil(rect.top),
+ (int) Math.floor(rect.right), (int) Math.floor(rect.bottom), radius);
+ }
+
+ @Override
+ protected void onResize(float w, float h) {
+ super.onResize(w, h);
+
+ RectF r = rect();
+ mPath.reset();
+
+ if (mOuterRadii != null) {
+ mPath.addRoundRect(r, mOuterRadii, Path.Direction.CW);
+ } else {
+ mPath.addRect(r, Path.Direction.CW);
+ }
+ if (mInnerRect != null) {
+ mInnerRect.set(r.left + mInset.left, r.top + mInset.top,
+ r.right - mInset.right, r.bottom - mInset.bottom);
+ if (mInnerRect.width() < w && mInnerRect.height() < h) {
+ if (mInnerRadii != null) {
+ mPath.addRoundRect(mInnerRect, mInnerRadii, Path.Direction.CCW);
+ } else {
+ mPath.addRect(mInnerRect, Path.Direction.CCW);
+ }
+ }
+ }
+ }
+
+ @Override
+ public RoundRectShape clone() throws CloneNotSupportedException {
+ final RoundRectShape shape = (RoundRectShape) super.clone();
+ shape.mOuterRadii = mOuterRadii != null ? mOuterRadii.clone() : null;
+ shape.mInnerRadii = mInnerRadii != null ? mInnerRadii.clone() : null;
+ shape.mInset = new RectF(mInset);
+ shape.mInnerRect = new RectF(mInnerRect);
+ shape.mPath = new Path(mPath);
+ return shape;
+ }
+}
diff --git a/android/graphics/drawable/shapes/Shape.java b/android/graphics/drawable/shapes/Shape.java
new file mode 100644
index 0000000..30b28f3
--- /dev/null
+++ b/android/graphics/drawable/shapes/Shape.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2007 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 android.graphics.drawable.shapes;
+
+import android.annotation.NonNull;
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Paint;
+
+/**
+ * Defines a generic graphical "shape."
+ * <p>
+ * Any Shape can be drawn to a Canvas with its own draw() method, but more
+ * graphical control is available if you instead pass it to a
+ * {@link android.graphics.drawable.ShapeDrawable}.
+ * <p>
+ * Custom Shape classes must implement {@link #clone()} and return an instance
+ * of the custom Shape class.
+ */
+public abstract class Shape implements Cloneable {
+ private float mWidth;
+ private float mHeight;
+
+ /**
+ * Returns the width of the Shape.
+ */
+ public final float getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Returns the height of the Shape.
+ */
+ public final float getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Draws this shape into the provided Canvas, with the provided Paint.
+ * <p>
+ * Before calling this, you must call {@link #resize(float,float)}.
+ *
+ * @param canvas the Canvas within which this shape should be drawn
+ * @param paint the Paint object that defines this shape's characteristics
+ */
+ public abstract void draw(Canvas canvas, Paint paint);
+
+ /**
+ * Resizes the dimensions of this shape.
+ * <p>
+ * Must be called before {@link #draw(Canvas,Paint)}.
+ *
+ * @param width the width of the shape (in pixels)
+ * @param height the height of the shape (in pixels)
+ */
+ public final void resize(float width, float height) {
+ if (width < 0) {
+ width = 0;
+ }
+ if (height < 0) {
+ height =0;
+ }
+ if (mWidth != width || mHeight != height) {
+ mWidth = width;
+ mHeight = height;
+ onResize(width, height);
+ }
+ }
+
+ /**
+ * Checks whether the Shape is opaque.
+ * <p>
+ * Default impl returns {@code true}. Override if your subclass can be
+ * opaque.
+ *
+ * @return true if any part of the drawable is <em>not</em> opaque.
+ */
+ public boolean hasAlpha() {
+ return true;
+ }
+
+ /**
+ * Callback method called when {@link #resize(float,float)} is executed.
+ *
+ * @param width the new width of the Shape
+ * @param height the new height of the Shape
+ */
+ protected void onResize(float width, float height) {}
+
+ /**
+ * Computes the Outline of the shape and return it in the supplied Outline
+ * parameter. The default implementation does nothing and {@code outline}
+ * is not changed.
+ *
+ * @param outline the Outline to be populated with the result. Must be
+ * non-{@code null}.
+ */
+ public void getOutline(@NonNull Outline outline) {}
+
+ @Override
+ public Shape clone() throws CloneNotSupportedException {
+ return (Shape) super.clone();
+ }
+}
diff --git a/android/graphics/fonts/FontVariationAxis.java b/android/graphics/fonts/FontVariationAxis.java
new file mode 100644
index 0000000..1b7408a
--- /dev/null
+++ b/android/graphics/fonts/FontVariationAxis.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2017 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 android.graphics.fonts;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * Class that holds information about single font variation axis.
+ */
+public final class FontVariationAxis {
+ private final int mTag;
+ private final String mTagString;
+ private final float mStyleValue;
+
+ /**
+ * Construct FontVariationAxis.
+ *
+ * The axis tag must contain four ASCII characters. Tag string that are longer or shorter than
+ * four characters, or contains characters outside of U+0020..U+007E are invalid.
+ *
+ * @throws IllegalArgumentException If given tag string is invalid.
+ */
+ public FontVariationAxis(@NonNull String tagString, float styleValue) {
+ if (!isValidTag(tagString)) {
+ throw new IllegalArgumentException("Illegal tag pattern: " + tagString);
+ }
+ mTag = makeTag(tagString);
+ mTagString = tagString;
+ mStyleValue = styleValue;
+ }
+
+ /**
+ * Returns the OpenType style tag value.
+ * @hide
+ */
+ public int getOpenTypeTagValue() {
+ return mTag;
+ }
+
+ /**
+ * Returns the variable font axis tag associated to this axis.
+ */
+ public String getTag() {
+ return mTagString;
+ }
+
+ /**
+ * Returns the style value associated to the given axis for this font.
+ */
+ public float getStyleValue() {
+ return mStyleValue;
+ }
+
+ /**
+ * Returns a valid font variation setting string for this object.
+ */
+ @Override
+ public @NonNull String toString() {
+ return "'" + mTagString + "' " + Float.toString(mStyleValue);
+ }
+
+ /**
+ * The 'tag' attribute value is read as four character values between U+0020 and U+007E
+ * inclusive.
+ */
+ private static final Pattern TAG_PATTERN = Pattern.compile("[\u0020-\u007E]{4}");
+
+ /**
+ * Returns true if 'tagString' is valid for font variation axis tag.
+ */
+ private static boolean isValidTag(String tagString) {
+ return tagString != null && TAG_PATTERN.matcher(tagString).matches();
+ }
+
+ /**
+ * The 'styleValue' attribute has an optional leading '-', followed by '<digits>',
+ * '<digits>.<digits>', or '.<digits>' where '<digits>' is one or more of [0-9].
+ */
+ private static final Pattern STYLE_VALUE_PATTERN =
+ Pattern.compile("-?(([0-9]+(\\.[0-9]+)?)|(\\.[0-9]+))");
+
+ private static boolean isValidValueFormat(String valueString) {
+ return valueString != null && STYLE_VALUE_PATTERN.matcher(valueString).matches();
+ }
+
+ /** @hide */
+ public static int makeTag(String tagString) {
+ final char c1 = tagString.charAt(0);
+ final char c2 = tagString.charAt(1);
+ final char c3 = tagString.charAt(2);
+ final char c4 = tagString.charAt(3);
+ return (c1 << 24) | (c2 << 16) | (c3 << 8) | c4;
+ }
+
+ /**
+ * Construct FontVariationAxis array from font variation settings.
+ *
+ * The settings string is constructed from multiple pairs of axis tag and style values. The axis
+ * tag must contain four ASCII characters and must be wrapped with single quotes (U+0027) or
+ * double quotes (U+0022). Axis strings that are longer or shorter than four characters, or
+ * contain characters outside of U+0020..U+007E are invalid. If a specified axis name is not
+ * defined in the font, the settings will be ignored.
+ *
+ * <pre>
+ * FontVariationAxis.fromFontVariationSettings("'wdth' 1.0");
+ * FontVariationAxis.fromFontVariationSettings("'AX ' 1.0, 'FB ' 2.0");
+ * </pre>
+ *
+ * @param settings font variation settings.
+ * @return FontVariationAxis[] the array of parsed font variation axis. {@code null} if settings
+ * has no font variation settings.
+ * @throws IllegalArgumentException If given string is not a valid font variation settings
+ * format.
+ */
+ public static @Nullable FontVariationAxis[] fromFontVariationSettings(
+ @Nullable String settings) {
+ if (settings == null || settings.isEmpty()) {
+ return null;
+ }
+ final ArrayList<FontVariationAxis> axisList = new ArrayList<>();
+ final int length = settings.length();
+ for (int i = 0; i < length; i++) {
+ final char c = settings.charAt(i);
+ if (Character.isWhitespace(c)) {
+ continue;
+ }
+ if (!(c == '\'' || c == '"') || length < i + 6 || settings.charAt(i + 5) != c) {
+ throw new IllegalArgumentException(
+ "Tag should be wrapped with double or single quote: " + settings);
+ }
+ final String tagString = settings.substring(i + 1, i + 5);
+
+ i += 6; // Move to end of tag.
+ int endOfValueString = settings.indexOf(',', i);
+ if (endOfValueString == -1) {
+ endOfValueString = length;
+ }
+ final float value;
+ try {
+ // Float.parseFloat ignores leading/trailing whitespaces.
+ value = Float.parseFloat(settings.substring(i, endOfValueString));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "Failed to parse float string: " + e.getMessage());
+ }
+ axisList.add(new FontVariationAxis(tagString, value));
+ i = endOfValueString;
+ }
+ if (axisList.isEmpty()) {
+ return null;
+ }
+ return axisList.toArray(new FontVariationAxis[0]);
+ }
+
+ /**
+ * Stringify the array of FontVariationAxis.
+ *
+ * @param axes an array of FontVariationAxis.
+ * @return String a valid font variation settings string.
+ */
+ public static @NonNull String toFontVariationSettings(@Nullable FontVariationAxis[] axes) {
+ if (axes == null) {
+ return "";
+ }
+ return TextUtils.join(",", axes);
+ }
+}
+
diff --git a/android/graphics/pdf/PdfDocument.java b/android/graphics/pdf/PdfDocument.java
new file mode 100644
index 0000000..1b8336f
--- /dev/null
+++ b/android/graphics/pdf/PdfDocument.java
@@ -0,0 +1,459 @@
+/*
+ * 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 android.graphics.pdf;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import dalvik.system.CloseGuard;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * <p>
+ * This class enables generating a PDF document from native Android content. You
+ * create a new document and then for every page you want to add you start a page,
+ * write content to the page, and finish the page. After you are done with all
+ * pages, you write the document to an output stream and close the document.
+ * After a document is closed you should not use it anymore. Note that pages are
+ * created one by one, i.e. you can have only a single page to which you are
+ * writing at any given time. This class is not thread safe.
+ * </p>
+ * <p>
+ * A typical use of the APIs looks like this:
+ * </p>
+ * <pre>
+ * // create a new document
+ * PdfDocument document = new PdfDocument();
+ *
+ * // crate a page description
+ * PageInfo pageInfo = new PageInfo.Builder(new Rect(0, 0, 100, 100), 1).create();
+ *
+ * // start a page
+ * Page page = document.startPage(pageInfo);
+ *
+ * // draw something on the page
+ * View content = getContentView();
+ * content.draw(page.getCanvas());
+ *
+ * // finish the page
+ * document.finishPage(page);
+ * . . .
+ * // add more pages
+ * . . .
+ * // write the document content
+ * document.writeTo(getOutputStream());
+ *
+ * // close the document
+ * document.close();
+ * </pre>
+ */
+public class PdfDocument {
+
+ // TODO: We need a constructor that will take an OutputStream to
+ // support online data serialization as opposed to the current
+ // on demand one. The current approach is fine until Skia starts
+ // to support online PDF generation at which point we need to
+ // handle this.
+
+ private final byte[] mChunk = new byte[4096];
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private final List<PageInfo> mPages = new ArrayList<PageInfo>();
+
+ private long mNativeDocument;
+
+ private Page mCurrentPage;
+
+ /**
+ * Creates a new instance.
+ */
+ public PdfDocument() {
+ mNativeDocument = nativeCreateDocument();
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * Starts a page using the provided {@link PageInfo}. After the page
+ * is created you can draw arbitrary content on the page's canvas which
+ * you can get by calling {@link Page#getCanvas()}. After you are done
+ * drawing the content you should finish the page by calling
+ * {@link #finishPage(Page)}. After the page is finished you should
+ * no longer access the page or its canvas.
+ * <p>
+ * <strong>Note:</strong> Do not call this method after {@link #close()}.
+ * Also do not call this method if the last page returned by this method
+ * is not finished by calling {@link #finishPage(Page)}.
+ * </p>
+ *
+ * @param pageInfo The page info. Cannot be null.
+ * @return A blank page.
+ *
+ * @see #finishPage(Page)
+ */
+ public Page startPage(PageInfo pageInfo) {
+ throwIfClosed();
+ throwIfCurrentPageNotFinished();
+ if (pageInfo == null) {
+ throw new IllegalArgumentException("page cannot be null");
+ }
+ Canvas canvas = new PdfCanvas(nativeStartPage(mNativeDocument, pageInfo.mPageWidth,
+ pageInfo.mPageHeight, pageInfo.mContentRect.left, pageInfo.mContentRect.top,
+ pageInfo.mContentRect.right, pageInfo.mContentRect.bottom));
+ mCurrentPage = new Page(canvas, pageInfo);
+ return mCurrentPage;
+ }
+
+ /**
+ * Finishes a started page. You should always finish the last started page.
+ * <p>
+ * <strong>Note:</strong> Do not call this method after {@link #close()}.
+ * You should not finish the same page more than once.
+ * </p>
+ *
+ * @param page The page. Cannot be null.
+ *
+ * @see #startPage(PageInfo)
+ */
+ public void finishPage(Page page) {
+ throwIfClosed();
+ if (page == null) {
+ throw new IllegalArgumentException("page cannot be null");
+ }
+ if (page != mCurrentPage) {
+ throw new IllegalStateException("invalid page");
+ }
+ if (page.isFinished()) {
+ throw new IllegalStateException("page already finished");
+ }
+ mPages.add(page.getInfo());
+ mCurrentPage = null;
+ nativeFinishPage(mNativeDocument);
+ page.finish();
+ }
+
+ /**
+ * Writes the document to an output stream. You can call this method
+ * multiple times.
+ * <p>
+ * <strong>Note:</strong> Do not call this method after {@link #close()}.
+ * Also do not call this method if a page returned by {@link #startPage(
+ * PageInfo)} is not finished by calling {@link #finishPage(Page)}.
+ * </p>
+ *
+ * @param out The output stream. Cannot be null.
+ *
+ * @throws IOException If an error occurs while writing.
+ */
+ public void writeTo(OutputStream out) throws IOException {
+ throwIfClosed();
+ throwIfCurrentPageNotFinished();
+ if (out == null) {
+ throw new IllegalArgumentException("out cannot be null!");
+ }
+ nativeWriteTo(mNativeDocument, out, mChunk);
+ }
+
+ /**
+ * Gets the pages of the document.
+ *
+ * @return The pages or an empty list.
+ */
+ public List<PageInfo> getPages() {
+ return Collections.unmodifiableList(mPages);
+ }
+
+ /**
+ * Closes this document. This method should be called after you
+ * are done working with the document. After this call the document
+ * is considered closed and none of its methods should be called.
+ * <p>
+ * <strong>Note:</strong> Do not call this method if the page
+ * returned by {@link #startPage(PageInfo)} is not finished by
+ * calling {@link #finishPage(Page)}.
+ * </p>
+ */
+ public void close() {
+ throwIfCurrentPageNotFinished();
+ dispose();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ dispose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void dispose() {
+ if (mNativeDocument != 0) {
+ nativeClose(mNativeDocument);
+ mCloseGuard.close();
+ mNativeDocument = 0;
+ }
+ }
+
+ /**
+ * Throws an exception if the document is already closed.
+ */
+ private void throwIfClosed() {
+ if (mNativeDocument == 0) {
+ throw new IllegalStateException("document is closed!");
+ }
+ }
+
+ /**
+ * Throws an exception if the last started page is not finished.
+ */
+ private void throwIfCurrentPageNotFinished() {
+ if (mCurrentPage != null) {
+ throw new IllegalStateException("Current page not finished!");
+ }
+ }
+
+ private native long nativeCreateDocument();
+
+ private native void nativeClose(long nativeDocument);
+
+ private native void nativeFinishPage(long nativeDocument);
+
+ private native void nativeWriteTo(long nativeDocument, OutputStream out, byte[] chunk);
+
+ private static native long nativeStartPage(long nativeDocument, int pageWidth, int pageHeight,
+ int contentLeft, int contentTop, int contentRight, int contentBottom);
+
+ private final class PdfCanvas extends Canvas {
+
+ public PdfCanvas(long nativeCanvas) {
+ super(nativeCanvas);
+ }
+
+ @Override
+ public void setBitmap(Bitmap bitmap) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * This class represents meta-data that describes a PDF {@link Page}.
+ */
+ public static final class PageInfo {
+ private int mPageWidth;
+ private int mPageHeight;
+ private Rect mContentRect;
+ private int mPageNumber;
+
+ /**
+ * Creates a new instance.
+ */
+ private PageInfo() {
+ /* do nothing */
+ }
+
+ /**
+ * Gets the page width in PostScript points (1/72th of an inch).
+ *
+ * @return The page width.
+ */
+ public int getPageWidth() {
+ return mPageWidth;
+ }
+
+ /**
+ * Gets the page height in PostScript points (1/72th of an inch).
+ *
+ * @return The page height.
+ */
+ public int getPageHeight() {
+ return mPageHeight;
+ }
+
+ /**
+ * Get the content rectangle in PostScript points (1/72th of an inch).
+ * This is the area that contains the page content and is relative to
+ * the page top left.
+ *
+ * @return The content rectangle.
+ */
+ public Rect getContentRect() {
+ return mContentRect;
+ }
+
+ /**
+ * Gets the page number.
+ *
+ * @return The page number.
+ */
+ public int getPageNumber() {
+ return mPageNumber;
+ }
+
+ /**
+ * Builder for creating a {@link PageInfo}.
+ */
+ public static final class Builder {
+ private final PageInfo mPageInfo = new PageInfo();
+
+ /**
+ * Creates a new builder with the mandatory page info attributes.
+ *
+ * @param pageWidth The page width in PostScript (1/72th of an inch).
+ * @param pageHeight The page height in PostScript (1/72th of an inch).
+ * @param pageNumber The page number.
+ */
+ public Builder(int pageWidth, int pageHeight, int pageNumber) {
+ if (pageWidth <= 0) {
+ throw new IllegalArgumentException("page width must be positive");
+ }
+ if (pageHeight <= 0) {
+ throw new IllegalArgumentException("page width must be positive");
+ }
+ if (pageNumber < 0) {
+ throw new IllegalArgumentException("pageNumber must be non negative");
+ }
+ mPageInfo.mPageWidth = pageWidth;
+ mPageInfo.mPageHeight = pageHeight;
+ mPageInfo.mPageNumber = pageNumber;
+ }
+
+ /**
+ * Sets the content rectangle in PostScript point (1/72th of an inch).
+ * This is the area that contains the page content and is relative to
+ * the page top left.
+ *
+ * @param contentRect The content rectangle. Must fit in the page.
+ */
+ public Builder setContentRect(Rect contentRect) {
+ if (contentRect != null && (contentRect.left < 0
+ || contentRect.top < 0
+ || contentRect.right > mPageInfo.mPageWidth
+ || contentRect.bottom > mPageInfo.mPageHeight)) {
+ throw new IllegalArgumentException("contentRect does not fit the page");
+ }
+ mPageInfo.mContentRect = contentRect;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link PageInfo}.
+ *
+ * @return The new instance.
+ */
+ public PageInfo create() {
+ if (mPageInfo.mContentRect == null) {
+ mPageInfo.mContentRect = new Rect(0, 0,
+ mPageInfo.mPageWidth, mPageInfo.mPageHeight);
+ }
+ return mPageInfo;
+ }
+ }
+ }
+
+ /**
+ * This class represents a PDF document page. It has associated
+ * a canvas on which you can draw content and is acquired by a
+ * call to {@link #getCanvas()}. It also has associated a
+ * {@link PageInfo} instance that describes its attributes. Also
+ * a page has
+ */
+ public static final class Page {
+ private final PageInfo mPageInfo;
+ private Canvas mCanvas;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param canvas The canvas of the page.
+ * @param pageInfo The info with meta-data.
+ */
+ private Page(Canvas canvas, PageInfo pageInfo) {
+ mCanvas = canvas;
+ mPageInfo = pageInfo;
+ }
+
+ /**
+ * Gets the {@link Canvas} of the page.
+ *
+ * <p>
+ * <strong>Note: </strong> There are some draw operations that are not yet
+ * supported by the canvas returned by this method. More specifically:
+ * <ul>
+ * <li>Inverse path clipping performed via {@link Canvas#clipPath(android.graphics.Path,
+ * android.graphics.Region.Op) Canvas.clipPath(android.graphics.Path,
+ * android.graphics.Region.Op)} for {@link
+ * android.graphics.Region.Op#REVERSE_DIFFERENCE
+ * Region.Op#REVERSE_DIFFERENCE} operations.</li>
+ * <li>{@link Canvas#drawVertices(android.graphics.Canvas.VertexMode, int,
+ * float[], int, float[], int, int[], int, short[], int, int,
+ * android.graphics.Paint) Canvas.drawVertices(
+ * android.graphics.Canvas.VertexMode, int, float[], int, float[],
+ * int, int[], int, short[], int, int, android.graphics.Paint)}</li>
+ * <li>Color filters set via {@link Paint#setColorFilter(
+ * android.graphics.ColorFilter)}</li>
+ * <li>Mask filters set via {@link Paint#setMaskFilter(
+ * android.graphics.MaskFilter)}</li>
+ * <li>Some XFER modes such as
+ * {@link android.graphics.PorterDuff.Mode#SRC_ATOP PorterDuff.Mode SRC},
+ * {@link android.graphics.PorterDuff.Mode#DST_ATOP PorterDuff.DST_ATOP},
+ * {@link android.graphics.PorterDuff.Mode#XOR PorterDuff.XOR},
+ * {@link android.graphics.PorterDuff.Mode#ADD PorterDuff.ADD}</li>
+ * </ul>
+ *
+ * @return The canvas if the page is not finished, null otherwise.
+ *
+ * @see PdfDocument#finishPage(Page)
+ */
+ public Canvas getCanvas() {
+ return mCanvas;
+ }
+
+ /**
+ * Gets the {@link PageInfo} with meta-data for the page.
+ *
+ * @return The page info.
+ *
+ * @see PdfDocument#finishPage(Page)
+ */
+ public PageInfo getInfo() {
+ return mPageInfo;
+ }
+
+ boolean isFinished() {
+ return mCanvas == null;
+ }
+
+ private void finish() {
+ if (mCanvas != null) {
+ mCanvas.release();
+ mCanvas = null;
+ }
+ }
+ }
+}
diff --git a/android/graphics/pdf/PdfEditor.java b/android/graphics/pdf/PdfEditor.java
new file mode 100644
index 0000000..0c509b7
--- /dev/null
+++ b/android/graphics/pdf/PdfEditor.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2014 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 android.graphics.pdf;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import dalvik.system.CloseGuard;
+import libcore.io.IoUtils;
+import libcore.io.Libcore;
+
+import java.io.IOException;
+
+/**
+ * Class for editing PDF files.
+ *
+ * @hide
+ */
+public final class PdfEditor {
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private long mNativeDocument;
+
+ private int mPageCount;
+
+ private ParcelFileDescriptor mInput;
+
+ /**
+ * Creates a new instance.
+ * <p>
+ * <strong>Note:</strong> The provided file descriptor must be <strong>seekable</strong>,
+ * i.e. its data being randomly accessed, e.g. pointing to a file. After finishing
+ * with this class you must call {@link #close()}.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> This class takes ownership of the passed in file descriptor
+ * and is responsible for closing it when the editor is closed.
+ * </p>
+ *
+ * @param input Seekable file descriptor to read from.
+ *
+ * @throws java.io.IOException If an error occurs while reading the file.
+ * @throws java.lang.SecurityException If the file requires a password or
+ * the security scheme is not supported.
+ *
+ * @see #close()
+ */
+ public PdfEditor(@NonNull ParcelFileDescriptor input) throws IOException {
+ if (input == null) {
+ throw new NullPointerException("input cannot be null");
+ }
+
+ final long size;
+ try {
+ Libcore.os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET);
+ size = Libcore.os.fstat(input.getFileDescriptor()).st_size;
+ } catch (ErrnoException ee) {
+ throw new IllegalArgumentException("file descriptor not seekable");
+ }
+ mInput = input;
+
+ synchronized (PdfRenderer.sPdfiumLock) {
+ mNativeDocument = nativeOpen(mInput.getFd(), size);
+ try {
+ mPageCount = nativeGetPageCount(mNativeDocument);
+ } catch (Throwable t) {
+ nativeClose(mNativeDocument);
+ mNativeDocument = 0;
+ throw t;
+ }
+ }
+
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * Gets the number of pages in the document.
+ *
+ * @return The page count.
+ */
+ public int getPageCount() {
+ throwIfClosed();
+ return mPageCount;
+ }
+
+ /**
+ * Removes the page with a given index.
+ *
+ * @param pageIndex The page to remove.
+ */
+ public void removePage(int pageIndex) {
+ throwIfClosed();
+ throwIfPageNotInDocument(pageIndex);
+
+ synchronized (PdfRenderer.sPdfiumLock) {
+ mPageCount = nativeRemovePage(mNativeDocument, pageIndex);
+ }
+ }
+
+ /**
+ * Sets a transformation and clip for a given page. The transformation matrix if
+ * non-null must be affine as per {@link android.graphics.Matrix#isAffine()}. If
+ * the clip is null, then no clipping is performed.
+ *
+ * @param pageIndex The page whose transform to set.
+ * @param transform The transformation to apply.
+ * @param clip The clip to apply.
+ */
+ public void setTransformAndClip(int pageIndex, @Nullable Matrix transform,
+ @Nullable Rect clip) {
+ throwIfClosed();
+ throwIfPageNotInDocument(pageIndex);
+ throwIfNotNullAndNotAfine(transform);
+ if (transform == null) {
+ transform = Matrix.IDENTITY_MATRIX;
+ }
+ if (clip == null) {
+ Point size = new Point();
+ getPageSize(pageIndex, size);
+
+ synchronized (PdfRenderer.sPdfiumLock) {
+ nativeSetTransformAndClip(mNativeDocument, pageIndex, transform.native_instance,
+ 0, 0, size.x, size.y);
+ }
+ } else {
+ synchronized (PdfRenderer.sPdfiumLock) {
+ nativeSetTransformAndClip(mNativeDocument, pageIndex, transform.native_instance,
+ clip.left, clip.top, clip.right, clip.bottom);
+ }
+ }
+ }
+
+ /**
+ * Gets the size of a given page in mils (1/72").
+ *
+ * @param pageIndex The page index.
+ * @param outSize The size output.
+ */
+ public void getPageSize(int pageIndex, @NonNull Point outSize) {
+ throwIfClosed();
+ throwIfOutSizeNull(outSize);
+ throwIfPageNotInDocument(pageIndex);
+
+ synchronized (PdfRenderer.sPdfiumLock) {
+ nativeGetPageSize(mNativeDocument, pageIndex, outSize);
+ }
+ }
+
+ /**
+ * Gets the media box of a given page in mils (1/72").
+ *
+ * @param pageIndex The page index.
+ * @param outMediaBox The media box output.
+ */
+ public boolean getPageMediaBox(int pageIndex, @NonNull Rect outMediaBox) {
+ throwIfClosed();
+ throwIfOutMediaBoxNull(outMediaBox);
+ throwIfPageNotInDocument(pageIndex);
+
+ synchronized (PdfRenderer.sPdfiumLock) {
+ return nativeGetPageMediaBox(mNativeDocument, pageIndex, outMediaBox);
+ }
+ }
+
+ /**
+ * Sets the media box of a given page in mils (1/72").
+ *
+ * @param pageIndex The page index.
+ * @param mediaBox The media box.
+ */
+ public void setPageMediaBox(int pageIndex, @NonNull Rect mediaBox) {
+ throwIfClosed();
+ throwIfMediaBoxNull(mediaBox);
+ throwIfPageNotInDocument(pageIndex);
+
+ synchronized (PdfRenderer.sPdfiumLock) {
+ nativeSetPageMediaBox(mNativeDocument, pageIndex, mediaBox);
+ }
+ }
+
+ /**
+ * Gets the crop box of a given page in mils (1/72").
+ *
+ * @param pageIndex The page index.
+ * @param outCropBox The crop box output.
+ */
+ public boolean getPageCropBox(int pageIndex, @NonNull Rect outCropBox) {
+ throwIfClosed();
+ throwIfOutCropBoxNull(outCropBox);
+ throwIfPageNotInDocument(pageIndex);
+
+ synchronized (PdfRenderer.sPdfiumLock) {
+ return nativeGetPageCropBox(mNativeDocument, pageIndex, outCropBox);
+ }
+ }
+
+ /**
+ * Sets the crop box of a given page in mils (1/72").
+ *
+ * @param pageIndex The page index.
+ * @param cropBox The crop box.
+ */
+ public void setPageCropBox(int pageIndex, @NonNull Rect cropBox) {
+ throwIfClosed();
+ throwIfCropBoxNull(cropBox);
+ throwIfPageNotInDocument(pageIndex);
+
+ synchronized (PdfRenderer.sPdfiumLock) {
+ nativeSetPageCropBox(mNativeDocument, pageIndex, cropBox);
+ }
+ }
+
+ /**
+ * Gets whether the document prefers to be scaled for printing.
+ *
+ * @return Whether to scale the document.
+ */
+ public boolean shouldScaleForPrinting() {
+ throwIfClosed();
+
+ synchronized (PdfRenderer.sPdfiumLock) {
+ return nativeScaleForPrinting(mNativeDocument);
+ }
+ }
+
+ /**
+ * Writes the PDF file to the provided destination.
+ * <p>
+ * <strong>Note:</strong> This method takes ownership of the passed in file
+ * descriptor and is responsible for closing it when writing completes.
+ * </p>
+ * @param output The destination.
+ */
+ public void write(ParcelFileDescriptor output) throws IOException {
+ try {
+ throwIfClosed();
+
+ synchronized (PdfRenderer.sPdfiumLock) {
+ nativeWrite(mNativeDocument, output.getFd());
+ }
+ } finally {
+ IoUtils.closeQuietly(output);
+ }
+ }
+
+ /**
+ * Closes this editor. You should not use this instance
+ * after this method is called.
+ */
+ public void close() {
+ throwIfClosed();
+ doClose();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ doClose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void doClose() {
+ if (mNativeDocument != 0) {
+ synchronized (PdfRenderer.sPdfiumLock) {
+ nativeClose(mNativeDocument);
+ }
+ mNativeDocument = 0;
+ }
+
+ if (mInput != null) {
+ IoUtils.closeQuietly(mInput);
+ mInput = null;
+ }
+ mCloseGuard.close();
+ }
+
+ private void throwIfClosed() {
+ if (mInput == null) {
+ throw new IllegalStateException("Already closed");
+ }
+ }
+
+ private void throwIfPageNotInDocument(int pageIndex) {
+ if (pageIndex < 0 || pageIndex >= mPageCount) {
+ throw new IllegalArgumentException("Invalid page index");
+ }
+ }
+
+ private void throwIfNotNullAndNotAfine(Matrix matrix) {
+ if (matrix != null && !matrix.isAffine()) {
+ throw new IllegalStateException("Matrix must be afine");
+ }
+ }
+
+ private void throwIfOutSizeNull(Point outSize) {
+ if (outSize == null) {
+ throw new NullPointerException("outSize cannot be null");
+ }
+ }
+
+ private void throwIfOutMediaBoxNull(Rect outMediaBox) {
+ if (outMediaBox == null) {
+ throw new NullPointerException("outMediaBox cannot be null");
+ }
+ }
+
+ private void throwIfMediaBoxNull(Rect mediaBox) {
+ if (mediaBox == null) {
+ throw new NullPointerException("mediaBox cannot be null");
+ }
+ }
+
+ private void throwIfOutCropBoxNull(Rect outCropBox) {
+ if (outCropBox == null) {
+ throw new NullPointerException("outCropBox cannot be null");
+ }
+ }
+
+ private void throwIfCropBoxNull(Rect cropBox) {
+ if (cropBox == null) {
+ throw new NullPointerException("cropBox cannot be null");
+ }
+ }
+
+ private static native long nativeOpen(int fd, long size);
+ private static native void nativeClose(long documentPtr);
+ private static native int nativeGetPageCount(long documentPtr);
+ private static native int nativeRemovePage(long documentPtr, int pageIndex);
+ private static native void nativeWrite(long documentPtr, int fd);
+ private static native void nativeSetTransformAndClip(long documentPtr, int pageIndex,
+ long transformPtr, int clipLeft, int clipTop, int clipRight, int clipBottom);
+ private static native void nativeGetPageSize(long documentPtr, int pageIndex, Point outSize);
+ private static native boolean nativeGetPageMediaBox(long documentPtr, int pageIndex,
+ Rect outMediaBox);
+ private static native void nativeSetPageMediaBox(long documentPtr, int pageIndex,
+ Rect mediaBox);
+ private static native boolean nativeGetPageCropBox(long documentPtr, int pageIndex,
+ Rect outMediaBox);
+ private static native void nativeSetPageCropBox(long documentPtr, int pageIndex,
+ Rect mediaBox);
+ private static native boolean nativeScaleForPrinting(long documentPtr);
+}
diff --git a/android/graphics/pdf/PdfRenderer.java b/android/graphics/pdf/PdfRenderer.java
new file mode 100644
index 0000000..c82ab0d
--- /dev/null
+++ b/android/graphics/pdf/PdfRenderer.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2014 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 android.graphics.pdf;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import com.android.internal.util.Preconditions;
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoUtils;
+import libcore.io.Libcore;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * <p>
+ * This class enables rendering a PDF document. This class is not thread safe.
+ * </p>
+ * <p>
+ * If you want to render a PDF, you create a renderer and for every page you want
+ * to render, you open the page, render it, and close the page. After you are done
+ * with rendering, you close the renderer. After the renderer is closed it should not
+ * be used anymore. Note that the pages are rendered one by one, i.e. you can have
+ * only a single page opened at any given time.
+ * </p>
+ * <p>
+ * A typical use of the APIs to render a PDF looks like this:
+ * </p>
+ * <pre>
+ * // create a new renderer
+ * PdfRenderer renderer = new PdfRenderer(getSeekableFileDescriptor());
+ *
+ * // let us just render all pages
+ * final int pageCount = renderer.getPageCount();
+ * for (int i = 0; i < pageCount; i++) {
+ * Page page = renderer.openPage(i);
+ *
+ * // say we render for showing on the screen
+ * page.render(mBitmap, null, null, Page.RENDER_MODE_FOR_DISPLAY);
+ *
+ * // do stuff with the bitmap
+ *
+ * // close the page
+ * page.close();
+ * }
+ *
+ * // close the renderer
+ * renderer.close();
+ * </pre>
+ *
+ * <h3>Print preview and print output</h3>
+ * <p>
+ * If you are using this class to rasterize a PDF for printing or show a print
+ * preview, it is recommended that you respect the following contract in order
+ * to provide a consistent user experience when seeing a preview and printing,
+ * i.e. the user sees a preview that is the same as the printout.
+ * </p>
+ * <ul>
+ * <li>
+ * Respect the property whether the document would like to be scaled for printing
+ * as per {@link #shouldScaleForPrinting()}.
+ * </li>
+ * <li>
+ * When scaling a document for printing the aspect ratio should be preserved.
+ * </li>
+ * <li>
+ * Do not inset the content with any margins from the {@link android.print.PrintAttributes}
+ * as the application is responsible to render it such that the margins are respected.
+ * </li>
+ * <li>
+ * If document page size is greater than the printed media size the content should
+ * be anchored to the upper left corner of the page for left-to-right locales and
+ * top right corner for right-to-left locales.
+ * </li>
+ * </ul>
+ *
+ * @see #close()
+ */
+public final class PdfRenderer implements AutoCloseable {
+ /**
+ * Any call the native pdfium code has to be single threaded as the library does not support
+ * parallel use.
+ */
+ final static Object sPdfiumLock = new Object();
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private final Point mTempPoint = new Point();
+
+ private long mNativeDocument;
+
+ private final int mPageCount;
+
+ private ParcelFileDescriptor mInput;
+
+ private Page mCurrentPage;
+
+ /** @hide */
+ @IntDef({
+ Page.RENDER_MODE_FOR_DISPLAY,
+ Page.RENDER_MODE_FOR_PRINT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RenderMode {}
+
+ /**
+ * Creates a new instance.
+ * <p>
+ * <strong>Note:</strong> The provided file descriptor must be <strong>seekable</strong>,
+ * i.e. its data being randomly accessed, e.g. pointing to a file.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> This class takes ownership of the passed in file descriptor
+ * and is responsible for closing it when the renderer is closed.
+ * </p>
+ * <p>
+ * If the file is from an untrusted source it is recommended to run the renderer in a separate,
+ * isolated process with minimal permissions to limit the impact of security exploits.
+ * </p>
+ *
+ * @param input Seekable file descriptor to read from.
+ *
+ * @throws java.io.IOException If an error occurs while reading the file.
+ * @throws java.lang.SecurityException If the file requires a password or
+ * the security scheme is not supported.
+ */
+ public PdfRenderer(@NonNull ParcelFileDescriptor input) throws IOException {
+ if (input == null) {
+ throw new NullPointerException("input cannot be null");
+ }
+
+ final long size;
+ try {
+ Libcore.os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET);
+ size = Libcore.os.fstat(input.getFileDescriptor()).st_size;
+ } catch (ErrnoException ee) {
+ throw new IllegalArgumentException("file descriptor not seekable");
+ }
+ mInput = input;
+
+ synchronized (sPdfiumLock) {
+ mNativeDocument = nativeCreate(mInput.getFd(), size);
+ try {
+ mPageCount = nativeGetPageCount(mNativeDocument);
+ } catch (Throwable t) {
+ nativeClose(mNativeDocument);
+ mNativeDocument = 0;
+ throw t;
+ }
+ }
+
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * Closes this renderer. You should not use this instance
+ * after this method is called.
+ */
+ public void close() {
+ throwIfClosed();
+ throwIfPageOpened();
+ doClose();
+ }
+
+ /**
+ * Gets the number of pages in the document.
+ *
+ * @return The page count.
+ */
+ public int getPageCount() {
+ throwIfClosed();
+ return mPageCount;
+ }
+
+ /**
+ * Gets whether the document prefers to be scaled for printing.
+ * You should take this info account if the document is rendered
+ * for printing and the target media size differs from the page
+ * size.
+ *
+ * @return If to scale the document.
+ */
+ public boolean shouldScaleForPrinting() {
+ throwIfClosed();
+
+ synchronized (sPdfiumLock) {
+ return nativeScaleForPrinting(mNativeDocument);
+ }
+ }
+
+ /**
+ * Opens a page for rendering.
+ *
+ * @param index The page index.
+ * @return A page that can be rendered.
+ *
+ * @see android.graphics.pdf.PdfRenderer.Page#close() PdfRenderer.Page.close()
+ */
+ public Page openPage(int index) {
+ throwIfClosed();
+ throwIfPageOpened();
+ throwIfPageNotInDocument(index);
+ mCurrentPage = new Page(index);
+ return mCurrentPage;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ doClose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void doClose() {
+ if (mCurrentPage != null) {
+ mCurrentPage.close();
+ mCurrentPage = null;
+ }
+
+ if (mNativeDocument != 0) {
+ synchronized (sPdfiumLock) {
+ nativeClose(mNativeDocument);
+ }
+ mNativeDocument = 0;
+ }
+
+ if (mInput != null) {
+ IoUtils.closeQuietly(mInput);
+ mInput = null;
+ }
+ mCloseGuard.close();
+ }
+
+ private void throwIfClosed() {
+ if (mInput == null) {
+ throw new IllegalStateException("Already closed");
+ }
+ }
+
+ private void throwIfPageOpened() {
+ if (mCurrentPage != null) {
+ throw new IllegalStateException("Current page not closed");
+ }
+ }
+
+ private void throwIfPageNotInDocument(int pageIndex) {
+ if (pageIndex < 0 || pageIndex >= mPageCount) {
+ throw new IllegalArgumentException("Invalid page index");
+ }
+ }
+
+ /**
+ * This class represents a PDF document page for rendering.
+ */
+ public final class Page implements AutoCloseable {
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ /**
+ * Mode to render the content for display on a screen.
+ */
+ public static final int RENDER_MODE_FOR_DISPLAY = 1;
+
+ /**
+ * Mode to render the content for printing.
+ */
+ public static final int RENDER_MODE_FOR_PRINT = 2;
+
+ private final int mIndex;
+ private final int mWidth;
+ private final int mHeight;
+
+ private long mNativePage;
+
+ private Page(int index) {
+ Point size = mTempPoint;
+ synchronized (sPdfiumLock) {
+ mNativePage = nativeOpenPageAndGetSize(mNativeDocument, index, size);
+ }
+ mIndex = index;
+ mWidth = size.x;
+ mHeight = size.y;
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * Gets the page index.
+ *
+ * @return The index.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * Gets the page width in points (1/72").
+ *
+ * @return The width in points.
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Gets the page height in points (1/72").
+ *
+ * @return The height in points.
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Renders a page to a bitmap.
+ * <p>
+ * You may optionally specify a rectangular clip in the bitmap bounds. No rendering
+ * outside the clip will be performed, hence it is your responsibility to initialize
+ * the bitmap outside the clip.
+ * </p>
+ * <p>
+ * You may optionally specify a matrix to transform the content from page coordinates
+ * which are in points (1/72") to bitmap coordinates which are in pixels. If this
+ * matrix is not provided this method will apply a transformation that will fit the
+ * whole page to the destination clip if provided or the destination bitmap if no
+ * clip is provided.
+ * </p>
+ * <p>
+ * The clip and transformation are useful for implementing tile rendering where the
+ * destination bitmap contains a portion of the image, for example when zooming.
+ * Another useful application is for printing where the size of the bitmap holding
+ * the page is too large and a client can render the page in stripes.
+ * </p>
+ * <p>
+ * <strong>Note: </strong> The destination bitmap format must be
+ * {@link Config#ARGB_8888 ARGB}.
+ * </p>
+ * <p>
+ * <strong>Note: </strong> The optional transformation matrix must be affine as per
+ * {@link android.graphics.Matrix#isAffine() Matrix.isAffine()}. Hence, you can specify
+ * rotation, scaling, translation but not a perspective transformation.
+ * </p>
+ *
+ * @param destination Destination bitmap to which to render.
+ * @param destClip Optional clip in the bitmap bounds.
+ * @param transform Optional transformation to apply when rendering.
+ * @param renderMode The render mode.
+ *
+ * @see #RENDER_MODE_FOR_DISPLAY
+ * @see #RENDER_MODE_FOR_PRINT
+ */
+ public void render(@NonNull Bitmap destination, @Nullable Rect destClip,
+ @Nullable Matrix transform, @RenderMode int renderMode) {
+ if (mNativePage == 0) {
+ throw new NullPointerException();
+ }
+
+ destination = Preconditions.checkNotNull(destination, "bitmap null");
+
+ if (destination.getConfig() != Config.ARGB_8888) {
+ throw new IllegalArgumentException("Unsupported pixel format");
+ }
+
+ if (destClip != null) {
+ if (destClip.left < 0 || destClip.top < 0
+ || destClip.right > destination.getWidth()
+ || destClip.bottom > destination.getHeight()) {
+ throw new IllegalArgumentException("destBounds not in destination");
+ }
+ }
+
+ if (transform != null && !transform.isAffine()) {
+ throw new IllegalArgumentException("transform not affine");
+ }
+
+ if (renderMode != RENDER_MODE_FOR_PRINT && renderMode != RENDER_MODE_FOR_DISPLAY) {
+ throw new IllegalArgumentException("Unsupported render mode");
+ }
+
+ if (renderMode == RENDER_MODE_FOR_PRINT && renderMode == RENDER_MODE_FOR_DISPLAY) {
+ throw new IllegalArgumentException("Only single render mode supported");
+ }
+
+ final int contentLeft = (destClip != null) ? destClip.left : 0;
+ final int contentTop = (destClip != null) ? destClip.top : 0;
+ final int contentRight = (destClip != null) ? destClip.right
+ : destination.getWidth();
+ final int contentBottom = (destClip != null) ? destClip.bottom
+ : destination.getHeight();
+
+ // If transform is not set, stretch page to whole clipped area
+ if (transform == null) {
+ int clipWidth = contentRight - contentLeft;
+ int clipHeight = contentBottom - contentTop;
+
+ transform = new Matrix();
+ transform.postScale((float)clipWidth / getWidth(),
+ (float)clipHeight / getHeight());
+ transform.postTranslate(contentLeft, contentTop);
+ }
+
+ final long transformPtr = transform.native_instance;
+
+ synchronized (sPdfiumLock) {
+ nativeRenderPage(mNativeDocument, mNativePage, destination, contentLeft,
+ contentTop, contentRight, contentBottom, transformPtr, renderMode);
+ }
+ }
+
+ /**
+ * Closes this page.
+ *
+ * @see android.graphics.pdf.PdfRenderer#openPage(int)
+ */
+ @Override
+ public void close() {
+ throwIfClosed();
+ doClose();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ doClose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void doClose() {
+ if (mNativePage != 0) {
+ synchronized (sPdfiumLock) {
+ nativeClosePage(mNativePage);
+ }
+ mNativePage = 0;
+ }
+
+ mCloseGuard.close();
+ mCurrentPage = null;
+ }
+
+ private void throwIfClosed() {
+ if (mNativePage == 0) {
+ throw new IllegalStateException("Already closed");
+ }
+ }
+ }
+
+ private static native long nativeCreate(int fd, long size);
+ private static native void nativeClose(long documentPtr);
+ private static native int nativeGetPageCount(long documentPtr);
+ private static native boolean nativeScaleForPrinting(long documentPtr);
+ private static native void nativeRenderPage(long documentPtr, long pagePtr, Bitmap dest,
+ int clipLeft, int clipTop, int clipRight, int clipBottom, long transformPtr,
+ int renderMode);
+ private static native long nativeOpenPageAndGetSize(long documentPtr, int pageIndex,
+ Point outSize);
+ private static native void nativeClosePage(long pagePtr);
+}
diff --git a/android/graphics/perftests/CanvasPerfTest.java b/android/graphics/perftests/CanvasPerfTest.java
new file mode 100644
index 0000000..eed1db0
--- /dev/null
+++ b/android/graphics/perftests/CanvasPerfTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.perftests;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Bitmap.Config;
+import android.graphics.Paint;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.filters.LargeTest;
+import android.view.DisplayListCanvas;
+import android.view.RenderNode;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+@LargeTest
+public class CanvasPerfTest {
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testBasicViewGroupDraw() {
+ // This test is a clone of BM_DisplayListCanvas_basicViewGroupDraw
+
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ RenderNode node = RenderNode.create("benchmark", null);
+ RenderNode child = RenderNode.create("child", null);
+ child.setLeftTopRightBottom(50, 50, 100, 100);
+
+ DisplayListCanvas canvas = node.start(100, 100);
+ node.end(canvas);
+ canvas = child.start(50, 50);
+ canvas.drawColor(Color.WHITE);
+ child.end(canvas);
+
+ while (state.keepRunning()) {
+ canvas = node.start(200, 200);
+ int save = canvas.save();
+ canvas.clipRect(1, 1, 199, 199);
+ canvas.insertReorderBarrier();
+ for (int i = 0; i < 5; i++) {
+ canvas.drawRenderNode(child);
+ }
+ canvas.insertInorderBarrier();
+ canvas.restoreToCount(save);
+ node.end(canvas);
+ }
+ }
+
+ @Test
+ public void testRecordSimpleBitmapView() {
+ // This test is a clone of BM_DisplayListCanvas_record_simpleBitmapView
+
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ RenderNode node = RenderNode.create("benchmark", null);
+
+ DisplayListCanvas canvas = node.start(100, 100);
+ node.end(canvas);
+ Bitmap bitmap = Bitmap.createBitmap(80, 80, Config.ARGB_8888);
+ Paint paint = new Paint();
+ paint.setColor(Color.BLACK);
+
+ while (state.keepRunning()) {
+ canvas = node.start(100, 100);
+ {
+ canvas.save();
+ canvas.drawRect(0, 0, 100, 100, paint);
+ canvas.restore();
+ }
+ {
+ canvas.save();
+ canvas.translate(10, 10);
+ canvas.drawBitmap(bitmap, 0, 0, null);
+ canvas.restore();
+ }
+ node.end(canvas);
+ }
+ }
+}
diff --git a/android/graphics/perftests/OutlinePerfTest.java b/android/graphics/perftests/OutlinePerfTest.java
new file mode 100644
index 0000000..3a4fc72
--- /dev/null
+++ b/android/graphics/perftests/OutlinePerfTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.perftests;
+
+import android.graphics.Outline;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.filters.LargeTest;
+import android.view.RenderNode;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+@LargeTest
+public class OutlinePerfTest {
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testSetEmpty() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ Outline outline = new Outline();
+ while (state.keepRunning()) {
+ outline.setEmpty();
+ }
+ }
+
+ @Test
+ public void testSetRoundRect() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Outline outline = new Outline();
+ while (state.keepRunning()) {
+ outline.setRoundRect(50, 50, 150, 150, 5);
+ }
+ }
+}
diff --git a/android/graphics/perftests/PaintHasGlyphPerfTest.java b/android/graphics/perftests/PaintHasGlyphPerfTest.java
new file mode 100644
index 0000000..26b8309
--- /dev/null
+++ b/android/graphics/perftests/PaintHasGlyphPerfTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.perftests;
+
+import android.graphics.Paint;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class PaintHasGlyphPerfTest {
+ @Parameters(name = "{0}")
+ public static Collection glyphStrings() {
+ return Arrays.asList(new Object[][] {
+ { "Latin", "A" },
+ { "Ligature", "fi" },
+ { "SurrogatePair", "\uD83D\uDE00" }, // U+1F600
+ { "Flags", "\uD83C\uDDFA\uD83C\uDDF8" }, // US
+ { "Ideograph_VariationSelector", "\u3402\uDB40\uDD00" }, // U+3402 U+E0100
+ { "Emoji_VariationSelector", "\u00A9\uFE0F" },
+ { "EmojiSequence",
+ // U+1F468 U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F468
+ "\uD83D\uDC68\u200D\u2764\uFE0F\u200D\uD83D\uDC8B\u200D\uD83D\uDC68" },
+ });
+ }
+
+ private final String mQuery;
+
+ public PaintHasGlyphPerfTest(String metricKey, String query) {
+ mQuery = query;
+ }
+
+ @Rule
+ public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testHasGlyph() {
+ Paint paint = new Paint();
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ paint.hasGlyph(mQuery);
+ }
+ }
+}
diff --git a/android/graphics/perftests/PaintMeasureTextTest.java b/android/graphics/perftests/PaintMeasureTextTest.java
new file mode 100644
index 0000000..b81908c
--- /dev/null
+++ b/android/graphics/perftests/PaintMeasureTextTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.perftests;
+
+import android.graphics.Canvas;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.filters.LargeTest;
+import android.text.TextPaint;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class PaintMeasureTextTest {
+
+ private static final int USE_CACHE = 0;
+ private static final int DONT_USE_CACHE = 1;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection measureSpecs() {
+ return Arrays.asList(new Object[][] {
+ { "alphabet_cached", USE_CACHE, "a" },
+ { "alphabet_not_cached", DONT_USE_CACHE, "a" },
+ // U+4E80 is an ideograph.
+ { "ideograph_cached", USE_CACHE, "\u4E80" },
+ { "ideograph_not_cached", DONT_USE_CACHE, "\u4E80" },
+ // U+20B9F(\uD842\uDF9F) is an ideograph.
+ { "surrogate_pairs_cached", USE_CACHE, "\uD842\uDF9F" },
+ { "surrogate_pairs_not_cached", DONT_USE_CACHE, "\uD842\uDF9F" },
+ // U+303D is PART ALTERNATION MARK
+ { "emoji_cached", USE_CACHE, "\u231A" },
+ { "emoji_not_cached", DONT_USE_CACHE, "\u231A" },
+ // U+1F368(\uD83C\uDF68) is ICE CREAM
+ { "emoji_surrogate_pairs_cached", USE_CACHE, "\uD83C\uDF68" },
+ { "emoji_surrogate_pairs_not_cached", DONT_USE_CACHE, "\uD83C\uDF68" },
+ });
+ }
+
+ private final String mText;
+ private final int mCacheMode;
+
+ public PaintMeasureTextTest(String key, int cacheMode, String text) {
+ mText = text;
+ mCacheMode = cacheMode;
+ }
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testMeasureTextPerf() {
+ TextPaint paint = new TextPaint();
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ if (mCacheMode == USE_CACHE) {
+ paint.measureText(mText);
+ } else {
+ Canvas.freeTextLayoutCaches();
+ }
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ if (mCacheMode == DONT_USE_CACHE) {
+ Canvas.freeTextLayoutCaches();
+ }
+ state.resumeTiming();
+
+ paint.measureText(mText);
+ }
+ }
+}
diff --git a/android/graphics/perftests/PathPerfTest.java b/android/graphics/perftests/PathPerfTest.java
new file mode 100644
index 0000000..7a49b4f
--- /dev/null
+++ b/android/graphics/perftests/PathPerfTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.perftests;
+
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.filters.LargeTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+@LargeTest
+public class PathPerfTest {
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testReset() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Path path = new Path();
+ while (state.keepRunning()) {
+ path.reset();
+ }
+ }
+
+ @Test
+ public void testAddReset() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Path path = new Path();
+ while (state.keepRunning()) {
+ path.addRect(0, 0, 100, 100, Path.Direction.CW);
+ path.reset();
+ }
+ }
+
+ @Test
+ public void testRewind() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Path path = new Path();
+ while (state.keepRunning()) {
+ path.rewind();
+ }
+ }
+
+ @Test
+ public void testAddRewind() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Path path = new Path();
+ while (state.keepRunning()) {
+ path.addRect(0, 0, 100, 100, Path.Direction.CW);
+ path.rewind();
+ }
+ }
+
+ @Test
+ public void testIsEmpty() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Path path = new Path();
+ path.addRect(0, 0, 100, 100, Path.Direction.CW);
+ while (state.keepRunning()) {
+ path.isEmpty();
+ }
+ }
+
+ @Test
+ public void testIsConvex() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Path path = new Path();
+ path.addRect(0, 0, 100, 100, Path.Direction.CW);
+ while (state.keepRunning()) {
+ path.isConvex();
+ }
+ }
+
+ @Test
+ public void testGetSetFillType() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Path path = new Path();
+ path.addRect(0, 0, 100, 100, Path.Direction.CW);
+ while (state.keepRunning()) {
+ path.setFillType(Path.FillType.EVEN_ODD);
+ path.getFillType();
+ }
+ }
+
+ @Test
+ public void testIsRect() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Path path = new Path();
+ path.addRect(0, 0, 100, 100, Path.Direction.CW);
+ final RectF outRect = new RectF();
+ while (state.keepRunning()) {
+ path.isRect(outRect);
+ }
+ }
+}
diff --git a/android/graphics/perftests/RenderNodePerfTest.java b/android/graphics/perftests/RenderNodePerfTest.java
new file mode 100644
index 0000000..dfbabeb
--- /dev/null
+++ b/android/graphics/perftests/RenderNodePerfTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.perftests;
+
+import android.graphics.Outline;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.filters.LargeTest;
+import android.view.DisplayListCanvas;
+import android.view.RenderNode;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+
+@LargeTest
+public class RenderNodePerfTest {
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testMeasureRenderNodeJniOverhead() {
+ final RenderNode node = RenderNode.create("benchmark", null);
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ node.setTranslationX(1.0f);
+ }
+ }
+
+ @Test
+ public void testCreateRenderNodeNoName() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ RenderNode node = RenderNode.create(null, null);
+ node.destroy();
+ }
+ }
+
+ @Test
+ public void testCreateRenderNode() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ RenderNode node = RenderNode.create("LinearLayout", null);
+ node.destroy();
+ }
+ }
+
+ @Test
+ public void testIsValid() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ RenderNode node = RenderNode.create("LinearLayout", null);
+ while (state.keepRunning()) {
+ node.isValid();
+ }
+ }
+
+ @Test
+ public void testStartEnd() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ RenderNode node = RenderNode.create("LinearLayout", null);
+ while (state.keepRunning()) {
+ DisplayListCanvas canvas = node.start(100, 100);
+ node.end(canvas);
+ }
+ }
+
+ @Test
+ public void testStartEndDeepHierarchy() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ RenderNode[] nodes = new RenderNode[30];
+ DisplayListCanvas[] canvases = new DisplayListCanvas[nodes.length];
+ for (int i = 0; i < nodes.length; i++) {
+ nodes[i] = RenderNode.create("LinearLayout", null);
+ }
+
+ while (state.keepRunning()) {
+ for (int i = 0; i < nodes.length; i++) {
+ canvases[i] = nodes[i].start(100, 100);
+ }
+ for (int i = nodes.length - 1; i >= 0; i--) {
+ nodes[i].end(canvases[i]);
+ }
+ }
+ }
+
+ @Test
+ public void testHasIdentityMatrix() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ RenderNode node = RenderNode.create("LinearLayout", null);
+ while (state.keepRunning()) {
+ node.hasIdentityMatrix();
+ }
+ }
+
+ @Test
+ public void testSetOutline() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ RenderNode node = RenderNode.create("LinearLayout", null);
+ Outline a = new Outline();
+ a.setRoundRect(0, 0, 100, 100, 10);
+ Outline b = new Outline();
+ b.setRect(50, 50, 150, 150);
+ b.setAlpha(0.5f);
+
+ while (state.keepRunning()) {
+ node.setOutline(a);
+ node.setOutline(b);
+ }
+ }
+}
diff --git a/android/graphics/perftests/TypefaceCreatePerfTest.java b/android/graphics/perftests/TypefaceCreatePerfTest.java
new file mode 100644
index 0000000..11ee599
--- /dev/null
+++ b/android/graphics/perftests/TypefaceCreatePerfTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.perftests;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Typeface;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TypefaceCreatePerfTest {
+ // A font file name in asset directory.
+ private static final String TEST_FONT_NAME = "DancingScript-Regular.ttf";
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testCreate_fromFamily() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ Typeface face = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
+ }
+ }
+
+ @Test
+ public void testCreate_fromFamilyName() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ Typeface face = Typeface.create("monospace", Typeface.NORMAL);
+ }
+ }
+
+ @Test
+ public void testCreate_fromAsset() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final Context context = InstrumentationRegistry.getContext();
+ final AssetManager am = context.getAssets();
+
+ while (state.keepRunning()) {
+ Typeface face = Typeface.createFromAsset(am, TEST_FONT_NAME);
+ }
+ }
+
+ @Test
+ public void testCreate_fromFile() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final Context context = InstrumentationRegistry.getContext();
+ final AssetManager am = context.getAssets();
+
+ File outFile = null;
+ try {
+ outFile = File.createTempFile("example", "ttf", context.getCacheDir());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ try (InputStream in = am.open(TEST_FONT_NAME);
+ OutputStream out = new FileOutputStream(outFile)) {
+ byte[] buf = new byte[1024];
+ int n = 0;
+ while ((n = in.read(buf)) != -1) {
+ out.write(buf, 0, n);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ while (state.keepRunning()) {
+ Typeface face = Typeface.createFromFile(outFile);
+ }
+
+ outFile.delete();
+ }
+}
diff --git a/android/graphics/perftests/VectorDrawablePerfTest.java b/android/graphics/perftests/VectorDrawablePerfTest.java
new file mode 100644
index 0000000..5533782
--- /dev/null
+++ b/android/graphics/perftests/VectorDrawablePerfTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.perftests;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.drawable.VectorDrawable;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.BitmapUtils;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.perftests.core.R;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+
+import static junit.framework.Assert.assertTrue;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class VectorDrawablePerfTest {
+
+ private static final boolean DUMP_BITMAP = false;
+
+ private int[] mTestWidths = {1024, 512};
+ private int[] mTestHeights = {512, 1024};
+
+ @Rule
+ public ActivityTestRule<StubActivity> mActivityRule =
+ new ActivityTestRule(StubActivity.class);
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testBitmapDrawPerf() {
+ int resId = R.drawable.vector_drawable01;
+ Activity activity = mActivityRule.getActivity();
+ VectorDrawable vd = (VectorDrawable) activity.getDrawable(resId);
+
+ int w = 1024, h = 1024;
+ Bitmap.Config conf = Bitmap.Config.ARGB_8888;
+ Bitmap bmp = Bitmap.createBitmap(w, h, conf);
+ Canvas canvas = new Canvas(bmp);
+
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ int i = 0;
+ while (state.keepRunning()) {
+ // Use different width / height each to force the vectorDrawable abandon the cache.
+ vd.setBounds(0, 0, mTestWidths[i % 2], mTestHeights[i % 2]);
+ i++;
+ vd.draw(canvas);
+ }
+
+ // Double check the bitmap pixels to make sure we draw correct content.
+ int backgroundColor = bmp.getPixel(w / 4, h / 2);
+ int objColor = bmp.getPixel(w / 8, h / 2 + 1);
+ int emptyColor = bmp.getPixel(w * 3 / 4, h * 3 / 4);
+ assertTrue("The background should be white", backgroundColor == Color.WHITE);
+ assertTrue("The object should be black", objColor == Color.BLACK);
+ assertTrue("The right bottom part should be empty", emptyColor == Color.TRANSPARENT);
+
+ if (DUMP_BITMAP) {
+ BitmapUtils.saveBitmapIntoPNG(activity, bmp, resId);
+ }
+ }
+}