diff options
Diffstat (limited to 'graphics/java')
57 files changed, 7769 insertions, 999 deletions
diff --git a/graphics/java/android/graphics/BaseCanvas.java b/graphics/java/android/graphics/BaseCanvas.java index 1745992022e1..3db240b54299 100644 --- a/graphics/java/android/graphics/BaseCanvas.java +++ b/graphics/java/android/graphics/BaseCanvas.java @@ -23,15 +23,15 @@ import android.annotation.Size; import android.annotation.UnsupportedAppUsage; import android.graphics.Canvas.VertexMode; import android.text.GraphicsOperations; +import android.text.MeasuredParagraph; import android.text.PrecomputedText; 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}. + * should be accompanied by a similar modification to {@link BaseRecordingCanvas}. * * 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. @@ -83,7 +83,7 @@ public abstract class BaseCanvas { // --------------------------------------------------------------------------- // Drawing methods - // These are also implemented in DisplayListCanvas so that we can + // These are also implemented in RecordingCanvas 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 @@ -376,6 +376,53 @@ public abstract class BaseCanvas { drawRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint); } + /** + * Make lint happy. + * See {@link Canvas#drawDoubleRoundRect(RectF, float, float, RectF, float, float, Paint)} + */ + public void drawDoubleRoundRect(@NonNull RectF outer, float outerRx, float outerRy, + @NonNull RectF inner, float innerRx, float innerRy, @NonNull Paint paint) { + throwIfHasHwBitmapInSwMode(paint); + float outerLeft = outer.left; + float outerTop = outer.top; + float outerRight = outer.right; + float outerBottom = outer.bottom; + + float innerLeft = inner.left; + float innerTop = inner.top; + float innerRight = inner.right; + float innerBottom = inner.bottom; + nDrawDoubleRoundRect(mNativeCanvasWrapper, outerLeft, outerTop, outerRight, outerBottom, + outerRx, outerRy, innerLeft, innerTop, innerRight, innerBottom, innerRx, innerRy, + paint.getNativeInstance()); + } + + /** + * Make lint happy. + * See {@link Canvas#drawDoubleRoundRect(RectF, float[], RectF, float[], Paint)} + */ + public void drawDoubleRoundRect(@NonNull RectF outer, float[] outerRadii, + @NonNull RectF inner, float[] innerRadii, @NonNull Paint paint) { + throwIfHasHwBitmapInSwMode(paint); + if (innerRadii == null || outerRadii == null + || innerRadii.length != 8 || outerRadii.length != 8) { + throw new IllegalArgumentException("Both inner and outer radii arrays must contain " + + "exactly 8 values"); + } + float outerLeft = outer.left; + float outerTop = outer.top; + float outerRight = outer.right; + float outerBottom = outer.bottom; + + float innerLeft = inner.left; + float innerTop = inner.top; + float innerRight = inner.right; + float innerBottom = inner.bottom; + nDrawDoubleRoundRect(mNativeCanvasWrapper, outerLeft, outerTop, outerRight, + outerBottom, outerRadii, innerLeft, innerTop, innerRight, innerBottom, innerRadii, + paint.getNativeInstance()); + } + public void drawText(@NonNull char[] text, int index, int count, float x, float y, @NonNull Paint paint) { if ((index | count | (index + count) | @@ -488,21 +535,31 @@ public abstract class BaseCanvas { ((GraphicsOperations) text).drawTextRun(this, start, end, contextStart, contextEnd, x, y, isRtl, paint); } else { + if (text instanceof PrecomputedText) { + final PrecomputedText pt = (PrecomputedText) text; + final int paraIndex = pt.findParaIndex(start); + if (end <= pt.getParagraphEnd(paraIndex)) { + final int paraStart = pt.getParagraphStart(paraIndex); + final MeasuredParagraph mp = pt.getMeasuredParagraph(paraIndex); + // Only support the text in the same paragraph. + nDrawTextRun(mNativeCanvasWrapper, + mp.getChars(), + start - paraStart, + end - start, + contextStart - paraStart, + contextEnd - contextStart, + x, y, isRtl, paint.getNativeInstance(), + mp.getMeasuredText().getNativePtr()); + return; + } + } int contextLen = contextEnd - contextStart; int len = end - start; char[] buf = TemporaryBuffer.obtain(contextLen); TextUtils.getChars(text, contextStart, contextEnd, buf, 0); - long measuredTextPtr = 0; - if (text instanceof PrecomputedText) { - PrecomputedText mt = (PrecomputedText) text; - int paraIndex = mt.findParaIndex(start); - if (end <= mt.getParagraphEnd(paraIndex)) { - // Only suppor the text in the same paragraph. - measuredTextPtr = mt.getMeasuredParagraph(paraIndex).getNativePtr(); - } - } nDrawTextRun(mNativeCanvasWrapper, buf, start - contextStart, len, - 0, contextLen, x, y, isRtl, paint.getNativeInstance(), measuredTextPtr); + 0, contextLen, x, y, isRtl, paint.getNativeInstance(), + 0 /* measured paragraph pointer */); TemporaryBuffer.recycle(buf); } } @@ -621,6 +678,16 @@ public abstract class BaseCanvas { 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 nDrawDoubleRoundRect(long nativeCanvas, float outerLeft, + float outerTop, float outerRight, float outerBottom, float outerRx, float outerRy, + float innerLeft, float innerTop, float innerRight, float innerBottom, float innerRx, + float innerRy, long nativePaint); + + private static native void nDrawDoubleRoundRect(long nativeCanvas, float outerLeft, + float outerTop, float outerRight, float outerBottom, float[] outerRadii, + float innerLeft, float innerTop, float innerRight, float innerBottom, + float[] innerRadii, long nativePaint); + private static native void nDrawPath(long nativeCanvas, long nativePath, long nativePaint); private static native void nDrawRegion(long nativeCanvas, long nativeRegion, long nativePaint); diff --git a/graphics/java/android/graphics/BaseRecordingCanvas.java b/graphics/java/android/graphics/BaseRecordingCanvas.java new file mode 100644 index 000000000000..4de7ca708eb6 --- /dev/null +++ b/graphics/java/android/graphics/BaseRecordingCanvas.java @@ -0,0 +1,674 @@ +/* + * 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.text.GraphicsOperations; +import android.text.MeasuredParagraph; +import android.text.PrecomputedText; +import android.text.SpannableString; +import android.text.SpannedString; +import android.text.TextUtils; + +import dalvik.annotation.optimization.FastNative; + +/** + * This class is a base class for canvases that defer drawing operations, so all + * the draw operations can be marked @FastNative. It contains a re-implementation of + * all the methods in {@link BaseCanvas}. + * + * @hide + */ +public class BaseRecordingCanvas extends Canvas { + + public BaseRecordingCanvas(long nativeCanvas) { + super(nativeCanvas); + } + + @Override + public final void drawArc(float left, float top, float right, float bottom, float startAngle, + float sweepAngle, boolean useCenter, @NonNull Paint paint) { + nDrawArc(mNativeCanvasWrapper, left, top, right, bottom, startAngle, sweepAngle, + useCenter, paint.getNativeInstance()); + } + + @Override + public final void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, + boolean useCenter, @NonNull Paint paint) { + drawArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, useCenter, + paint); + } + + @Override + public final void drawARGB(int a, int r, int g, int b) { + drawColor(Color.argb(a, r, g, b)); + } + + @Override + public final void drawBitmap(@NonNull Bitmap bitmap, float left, float top, + @Nullable Paint paint) { + throwIfCannotDraw(bitmap); + nDrawBitmap(mNativeCanvasWrapper, bitmap, left, top, + paint != null ? paint.getNativeInstance() : 0, mDensity, mScreenDensity, + bitmap.mDensity); + } + + @Override + public final void drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, + @Nullable Paint paint) { + nDrawBitmapMatrix(mNativeCanvasWrapper, bitmap, matrix.ni(), + paint != null ? paint.getNativeInstance() : 0); + } + + @Override + public final void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst, + @Nullable Paint paint) { + if (dst == null) { + throw new NullPointerException(); + } + throwIfCannotDraw(bitmap); + 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); + } + + @Override + public final void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst, + @Nullable Paint paint) { + if (dst == null) { + throw new NullPointerException(); + } + throwIfCannotDraw(bitmap); + 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 checkstyle */ + @Override + @Deprecated + public final 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(); + } + // 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 checkstyle */ + @Override + @Deprecated + public final 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); + } + + @Override + public final 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(); + } + 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); + } + + @Override + public final void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) { + nDrawCircle(mNativeCanvasWrapper, cx, cy, radius, paint.getNativeInstance()); + } + + @Override + public final void drawColor(@ColorInt int color) { + nDrawColor(mNativeCanvasWrapper, color, PorterDuff.Mode.SRC_OVER.nativeInt); + } + + @Override + public final void drawColor(@ColorInt int color, @NonNull PorterDuff.Mode mode) { + nDrawColor(mNativeCanvasWrapper, color, mode.nativeInt); + } + + @Override + public final void drawLine(float startX, float startY, float stopX, float stopY, + @NonNull Paint paint) { + nDrawLine(mNativeCanvasWrapper, startX, startY, stopX, stopY, paint.getNativeInstance()); + } + + @Override + public final void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count, + @NonNull Paint paint) { + nDrawLines(mNativeCanvasWrapper, pts, offset, count, paint.getNativeInstance()); + } + + @Override + public final void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint) { + drawLines(pts, 0, pts.length, paint); + } + + @Override + public final void drawOval(float left, float top, float right, float bottom, + @NonNull Paint paint) { + nDrawOval(mNativeCanvasWrapper, left, top, right, bottom, paint.getNativeInstance()); + } + + @Override + public final void drawOval(@NonNull RectF oval, @NonNull Paint paint) { + if (oval == null) { + throw new NullPointerException(); + } + drawOval(oval.left, oval.top, oval.right, oval.bottom, paint); + } + + @Override + public final void drawPaint(@NonNull Paint paint) { + nDrawPaint(mNativeCanvasWrapper, paint.getNativeInstance()); + } + + @Override + public final void drawPatch(@NonNull NinePatch patch, @NonNull Rect dst, + @Nullable Paint paint) { + Bitmap bitmap = patch.getBitmap(); + throwIfCannotDraw(bitmap); + 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()); + } + + @Override + public final void drawPatch(@NonNull NinePatch patch, @NonNull RectF dst, + @Nullable Paint paint) { + Bitmap bitmap = patch.getBitmap(); + throwIfCannotDraw(bitmap); + 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()); + } + + @Override + public final void drawPath(@NonNull Path path, @NonNull Paint paint) { + if (path.isSimplePath && path.rects != null) { + nDrawRegion(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance()); + } else { + nDrawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance()); + } + } + + @Override + public final void drawPicture(@NonNull Picture picture) { + picture.endRecording(); + int restoreCount = save(); + picture.draw(this); + restoreToCount(restoreCount); + } + + @Override + public final 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(); + } + + @Override + public final 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(); + } + + @Override + public final void drawPoint(float x, float y, @NonNull Paint paint) { + nDrawPoint(mNativeCanvasWrapper, x, y, paint.getNativeInstance()); + } + + @Override + public final void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count, + @NonNull Paint paint) { + nDrawPoints(mNativeCanvasWrapper, pts, offset, count, paint.getNativeInstance()); + } + + @Override + public final void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint) { + drawPoints(pts, 0, pts.length, paint); + } + + /** @deprecated checkstyle */ + @Override + @Deprecated + public final 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(); + } + for (int i = 0; i < count; i++) { + drawText(text, index + i, 1, pos[i * 2], pos[i * 2 + 1], paint); + } + } + + /** @deprecated checkstyle */ + @Override + @Deprecated + public final void drawPosText(@NonNull String text, @NonNull @Size(multiple = 2) float[] pos, + @NonNull Paint paint) { + drawPosText(text.toCharArray(), 0, text.length(), pos, paint); + } + + @Override + public final void drawRect(float left, float top, float right, float bottom, + @NonNull Paint paint) { + nDrawRect(mNativeCanvasWrapper, left, top, right, bottom, paint.getNativeInstance()); + } + + @Override + public final void drawRect(@NonNull Rect r, @NonNull Paint paint) { + drawRect(r.left, r.top, r.right, r.bottom, paint); + } + + @Override + public final void drawRect(@NonNull RectF rect, @NonNull Paint paint) { + nDrawRect(mNativeCanvasWrapper, + rect.left, rect.top, rect.right, rect.bottom, paint.getNativeInstance()); + } + + @Override + public final void drawRGB(int r, int g, int b) { + drawColor(Color.rgb(r, g, b)); + } + + @Override + public final void drawRoundRect(float left, float top, float right, float bottom, + float rx, float ry, @NonNull Paint paint) { + nDrawRoundRect(mNativeCanvasWrapper, left, top, right, bottom, rx, ry, + paint.getNativeInstance()); + } + + @Override + public final void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) { + drawRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint); + } + + @Override + public final void drawDoubleRoundRect(@NonNull RectF outer, float outerRx, float outerRy, + @NonNull RectF inner, float innerRx, float innerRy, @NonNull Paint paint) { + nDrawDoubleRoundRect(mNativeCanvasWrapper, + outer.left, outer.top, outer.right, outer.bottom, outerRx, outerRy, + inner.left, inner.top, inner.right, inner.bottom, innerRx, innerRy, + paint.getNativeInstance()); + } + + @Override + public final void drawDoubleRoundRect(@NonNull RectF outer, float[] outerRadii, + @NonNull RectF inner, float[] innerRadii, @NonNull Paint paint) { + nDrawDoubleRoundRect(mNativeCanvasWrapper, + outer.left, outer.top, outer.right, outer.bottom, outerRadii, + inner.left, inner.top, inner.right, inner.bottom, innerRadii, + paint.getNativeInstance()); + } + + @Override + public final 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(); + } + nDrawText(mNativeCanvasWrapper, text, index, count, x, y, paint.mBidiFlags, + paint.getNativeInstance()); + } + + @Override + public final 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(); + } + 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); + } + } + + @Override + public final void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) { + nDrawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags, + paint.getNativeInstance()); + } + + @Override + public final 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(); + } + nDrawText(mNativeCanvasWrapper, text, start, end, x, y, paint.mBidiFlags, + paint.getNativeInstance()); + } + + @Override + public final 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(); + } + nDrawTextOnPath(mNativeCanvasWrapper, text, index, count, + path.readOnlyNI(), hOffset, vOffset, + paint.mBidiFlags, paint.getNativeInstance()); + } + + @Override + public final void drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset, + float vOffset, @NonNull Paint paint) { + if (text.length() > 0) { + nDrawTextOnPath(mNativeCanvasWrapper, text, path.readOnlyNI(), hOffset, vOffset, + paint.mBidiFlags, paint.getNativeInstance()); + } + } + + @Override + public final 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(); + } + + nDrawTextRun(mNativeCanvasWrapper, text, index, count, contextIndex, contextCount, + x, y, isRtl, paint.getNativeInstance(), 0 /* measured text */); + } + + @Override + public final 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(); + } + + 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 { + if (text instanceof PrecomputedText) { + final PrecomputedText pt = (PrecomputedText) text; + final int paraIndex = pt.findParaIndex(start); + if (end <= pt.getParagraphEnd(paraIndex)) { + final int paraStart = pt.getParagraphStart(paraIndex); + final MeasuredParagraph mp = pt.getMeasuredParagraph(paraIndex); + // Only support if the target is in the same paragraph. + nDrawTextRun(mNativeCanvasWrapper, + mp.getChars(), + start - paraStart, + end - start, + contextStart - paraStart, + contextEnd - contextStart, + x, y, isRtl, paint.getNativeInstance(), + mp.getMeasuredText().getNativePtr()); + return; + } + } + 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(), + 0 /* measured paragraph pointer */); + TemporaryBuffer.recycle(buf); + } + } + + @Override + public final 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 (texs != null) { + checkRange(texs.length, texOffset, vertexCount); + } + if (colors != null) { + checkRange(colors.length, colorOffset, vertexCount / 2); + } + if (indices != null) { + checkRange(indices.length, indexOffset, indexCount); + } + nDrawVertices(mNativeCanvasWrapper, mode.nativeInt, vertexCount, verts, + vertOffset, texs, texOffset, colors, colorOffset, + indices, indexOffset, indexCount, paint.getNativeInstance()); + } + + @FastNative + private static native void nDrawBitmap(long nativeCanvas, Bitmap bitmap, float left, float top, + long nativePaintOrZero, int canvasDensity, int screenDensity, int bitmapDensity); + + @FastNative + 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); + + @FastNative + 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); + + @FastNative + private static native void nDrawColor(long nativeCanvas, int color, int mode); + + @FastNative + private static native void nDrawPaint(long nativeCanvas, long nativePaint); + + @FastNative + private static native void nDrawPoint(long canvasHandle, float x, float y, long paintHandle); + + @FastNative + private static native void nDrawPoints(long canvasHandle, float[] pts, int offset, int count, + long paintHandle); + + @FastNative + private static native void nDrawLine(long nativeCanvas, float startX, float startY, float stopX, + float stopY, long nativePaint); + + @FastNative + private static native void nDrawLines(long canvasHandle, float[] pts, int offset, int count, + long paintHandle); + + @FastNative + private static native void nDrawRect(long nativeCanvas, float left, float top, float right, + float bottom, long nativePaint); + + @FastNative + private static native void nDrawOval(long nativeCanvas, float left, float top, float right, + float bottom, long nativePaint); + + @FastNative + private static native void nDrawCircle(long nativeCanvas, float cx, float cy, float radius, + long nativePaint); + + @FastNative + private static native void nDrawArc(long nativeCanvas, float left, float top, float right, + float bottom, float startAngle, float sweep, boolean useCenter, long nativePaint); + + @FastNative + private static native void nDrawRoundRect(long nativeCanvas, float left, float top, float right, + float bottom, float rx, float ry, long nativePaint); + + @FastNative + private static native void nDrawDoubleRoundRect(long nativeCanvas, + float outerLeft, float outerTop, float outerRight, float outerBottom, + float outerRx, float outerRy, float innerLeft, float innerTop, float innerRight, + float innerBottom, float innerRx, float innerRy, long nativePaint); + + @FastNative + private static native void nDrawDoubleRoundRect(long nativeCanvas, float outerLeft, + float outerTop, float outerRight, float outerBottom, float[] outerRadii, + float innerLeft, float innerTop, float innerRight, float innerBottom, + float[] innerRadii, long nativePaint); + + @FastNative + private static native void nDrawPath(long nativeCanvas, long nativePath, long nativePaint); + + @FastNative + private static native void nDrawRegion(long nativeCanvas, long nativeRegion, long nativePaint); + + @FastNative + 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); + + @FastNative + private static native void nDrawBitmapMatrix(long nativeCanvas, Bitmap bitmap, + long nativeMatrix, long nativePaint); + + @FastNative + private static native void nDrawBitmapMesh(long nativeCanvas, Bitmap bitmap, int meshWidth, + int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, + long nativePaint); + + @FastNative + 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); + + @FastNative + private static native void nDrawText(long nativeCanvas, char[] text, int index, int count, + float x, float y, int flags, long nativePaint); + + @FastNative + private static native void nDrawText(long nativeCanvas, String text, int start, int end, + float x, float y, int flags, long nativePaint); + + @FastNative + 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); + + @FastNative + 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, + long nativePrecomputedText); + + @FastNative + private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count, + long nativePath, float hOffset, float vOffset, int bidiFlags, long nativePaint); + + @FastNative + private static native void nDrawTextOnPath(long nativeCanvas, String text, long nativePath, + float hOffset, float vOffset, int flags, long nativePaint); +} diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index d6131cf2e5d4..790b37eec4c5 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -24,16 +24,17 @@ import android.annotation.Size; import android.annotation.UnsupportedAppUsage; import android.annotation.WorkerThread; import android.content.res.ResourcesImpl; +import android.hardware.HardwareBuffer; 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 android.view.DisplayListCanvas; -import android.view.RenderNode; import android.view.ThreadedRenderer; +import dalvik.annotation.optimization.CriticalNative; + import libcore.util.NativeAllocationRegistry; import java.io.OutputStream; @@ -61,8 +62,6 @@ public final class Bitmap implements Parcelable { @UnsupportedAppUsage 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 @@ -125,8 +124,8 @@ public final class Bitmap implements Parcelable { * int (pointer). */ // called from JNI - Bitmap(long nativeBitmap, int width, int height, int density, - boolean isMutable, boolean requestPremultiplied, + @UnsupportedAppUsage + Bitmap(long nativeBitmap, int width, int height, int density, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) { if (nativeBitmap == 0) { throw new RuntimeException("internal error: native bitmap is 0"); @@ -134,7 +133,6 @@ public final class Bitmap implements Parcelable { mWidth = width; mHeight = height; - mIsMutable = isMutable; mRequestPremultiplied = requestPremultiplied; mNinePatchChunk = ninePatchChunk; @@ -356,14 +354,9 @@ public final class Bitmap implements Parcelable { * 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; - } + if (!mRecycled) { + nativeRecycle(mNativePtr); + mNinePatchChunk = null; mRecycled = true; } } @@ -730,6 +723,48 @@ public final class Bitmap implements Parcelable { } /** + * Create a hardware bitmap backed by a {@link HardwareBuffer}. + * + * <p>The passed HardwareBuffer's usage flags must contain + * {@link HardwareBuffer#USAGE_GPU_SAMPLED_IMAGE}. + * + * <p>The bitmap will keep a reference to the buffer so that callers can safely close the + * HardwareBuffer without affecting the Bitmap. However the HardwareBuffer must not be + * modified while a wrapped Bitmap is accessing it. Doing so will result in undefined behavior. + * + * @param hardwareBuffer The HardwareBuffer to wrap. + * @param colorSpace The color space of the bitmap. Must be a {@link ColorSpace.Rgb} colorspace. + * If null, SRGB is assumed. + * @return A bitmap wrapping the buffer, or null if there was a problem creating the bitmap. + * @throws IllegalArgumentException if the HardwareBuffer has an invalid usage, or an invalid + * colorspace is given. + */ + @Nullable + public static Bitmap wrapHardwareBuffer(@NonNull HardwareBuffer hardwareBuffer, + @Nullable ColorSpace colorSpace) { + if ((hardwareBuffer.getUsage() & HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE) == 0) { + throw new IllegalArgumentException("usage flags must contain USAGE_GPU_SAMPLED_IMAGE."); + } + int format = hardwareBuffer.getFormat(); + ColorSpace.Rgb rgb = null; + if (colorSpace != null) { + if (!(colorSpace instanceof ColorSpace.Rgb)) { + throw new IllegalArgumentException("colorSpace must be an RGB color space"); + } + rgb = (ColorSpace.Rgb) colorSpace; + } else { + rgb = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB); + } + 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); + return nativeWrapHardwareBufferBitmap(hardwareBuffer, d50.getTransform(), parameters); + } + + /** * 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 @@ -763,7 +798,7 @@ public final class Bitmap implements Parcelable { } /** - * Returns an immutable bitmap from the source bitmap. The new bitmap may + * Returns a 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. */ @@ -772,7 +807,7 @@ public final class Bitmap implements Parcelable { } /** - * Returns an immutable bitmap from the specified subset of the source + * Returns a 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. @@ -792,7 +827,7 @@ public final class Bitmap implements Parcelable { } /** - * Returns an immutable bitmap from subset of the source bitmap, + * Returns a 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 @@ -802,6 +837,12 @@ public final class Bitmap implements Parcelable { * same as the source bitmap itself, then the source bitmap is * returned and no new bitmap is created. * + * The returned bitmap will always be mutable except in the following scenarios: + * (1) In situations where the source bitmap is returned and the source bitmap is immutable + * + * (2) The source bitmap is a hardware bitmap. That is {@link #getConfig()} is equivalent to + * {@link Config#HARDWARE} + * * @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 @@ -1239,11 +1280,9 @@ public final class Bitmap implements Parcelable { * scaled to match if necessary. * @param height The height of the bitmap to create. The picture's height will be * scaled to match if necessary. - * @param config The {@link Config} of the created bitmap. If this is null then - * the bitmap will be {@link Config#HARDWARE}. + * @param config The {@link Config} of the created bitmap. * - * @return An immutable bitmap with a HARDWARE config whose contents are created - * from the recorded drawing commands in the Picture source. + * @return An immutable bitmap with a configuration specified by the config parameter */ public static @NonNull Bitmap createBitmap(@NonNull Picture source, int width, int height, @NonNull Config config) { @@ -1261,13 +1300,14 @@ public final class Bitmap implements Parcelable { final RenderNode node = RenderNode.create("BitmapTemporary", null); node.setLeftTopRightBottom(0, 0, width, height); node.setClipToBounds(false); - final DisplayListCanvas canvas = node.start(width, height); + node.setForceDarkAllowed(false); + final RecordingCanvas canvas = node.startRecording(width, height); if (source.getWidth() != width || source.getHeight() != height) { canvas.scale(width / (float) source.getWidth(), height / (float) source.getHeight()); } canvas.drawPicture(source); - node.end(canvas); + node.endRecording(); Bitmap bitmap = ThreadedRenderer.createHardwareBitmap(node, width, height); if (config != Config.HARDWARE) { bitmap = bitmap.copy(config, false); @@ -1282,7 +1322,7 @@ public final class Bitmap implements Parcelable { } canvas.drawPicture(source); canvas.setBitmap(null); - bitmap.makeImmutable(); + bitmap.setImmutable(); return bitmap; } } @@ -1373,13 +1413,22 @@ public final class Bitmap implements Parcelable { * Returns true if the bitmap is marked as mutable (i.e. can be drawn into) */ public final boolean isMutable() { - return mIsMutable; + return !nativeIsImmutable(mNativePtr); } - /** @hide */ - public final void makeImmutable() { - // todo mIsMutable = false; - // todo nMakeImmutable(); + /** + * Marks the Bitmap as immutable. Further modifications to this Bitmap are disallowed. + * After this method is called, this Bitmap cannot be made mutable again and subsequent calls + * to {@link #reconfigure(int, int, Config)}, {@link #setPixel(int, int, int)}, + * {@link #setPixels(int[], int, int, int, int, int, int)} and {@link #eraseColor(int)} will + * fail and throw an IllegalStateException. + * + * @hide + */ + public void setImmutable() { + if (isMutable()) { + nativeSetImmutable(mNativePtr); + } } /** @@ -1946,7 +1995,7 @@ public final class Bitmap implements Parcelable { public void writeToParcel(Parcel p, int flags) { checkRecycled("Can't parcel a recycled bitmap"); noteHardwareBitmapSlowCall(); - if (!nativeWriteToParcel(mNativePtr, mIsMutable, mDensity, p)) { + if (!nativeWriteToParcel(mNativePtr, isMutable(), mDensity, p)) { throw new RuntimeException("native writeToParcel failed"); } } @@ -2065,7 +2114,7 @@ public final class Bitmap implements Parcelable { 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 nativeRecycle(long nativeBitmap); @UnsupportedAppUsage private static native void nativeReconfigure(long nativeBitmap, int width, int height, int config, boolean isPremultiplied); @@ -2116,9 +2165,19 @@ public final class Bitmap implements Parcelable { private static native int nativeGetAllocationByteCount(long nativeBitmap); private static native Bitmap nativeCopyPreserveInternalConfig(long nativeBitmap); private static native Bitmap nativeCreateHardwareBitmap(GraphicBuffer buffer); + private static native Bitmap nativeWrapHardwareBufferBitmap(HardwareBuffer buffer, + @Size(9) float[] xyzD50, + ColorSpace.Rgb.TransferParameters p); 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 boolean nativeIsSRGBLinear(long nativePtr); private static native void nativeCopyColorSpace(long srcBitmap, long dstBitmap); + + private static native void nativeSetImmutable(long nativePtr); + + // ---------------- @CriticalNative ------------------- + + @CriticalNative + private static native boolean nativeIsImmutable(long nativePtr); } diff --git a/graphics/java/android/graphics/Camera.java b/graphics/java/android/graphics/Camera.java index 33889410a54b..cbd4eadca30a 100644 --- a/graphics/java/android/graphics/Camera.java +++ b/graphics/java/android/graphics/Camera.java @@ -24,8 +24,6 @@ import android.annotation.UnsupportedAppUsage; * {@link Canvas}. */ public class Camera { - private Matrix mMatrix; - /** * Creates a new camera, with empty transformations. */ @@ -151,13 +149,7 @@ public class Camera { * @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()); - } + nativeApplyToCanvas(canvas.getNativeCanvasWrapper()); } public native float dotWithNormal(float dx, float dy, float dz); diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java index 36c1c2133c2c..3b0dc9d9f125 100644 --- a/graphics/java/android/graphics/Canvas.java +++ b/graphics/java/android/graphics/Canvas.java @@ -205,11 +205,70 @@ public class Canvas extends BaseCanvas { mBitmap = bitmap; } - /** @hide */ - public void insertReorderBarrier() {} + /** + * @deprecated use {@link #enableZ()} instead + * @hide */ + @Deprecated + public void insertReorderBarrier() { + enableZ(); + } - /** @hide */ - public void insertInorderBarrier() {} + /** + * @deprecated use {@link #disableZ()} instead + * @hide */ + @Deprecated + public void insertInorderBarrier() { + disableZ(); + } + + /** + * <p>Enables Z support which defaults to disabled. This allows for RenderNodes drawn with + * {@link #drawRenderNode(RenderNode)} to be re-arranged based off of their + * {@link RenderNode#getElevation()} and {@link RenderNode#getTranslationZ()} + * values. It also enables rendering of shadows for RenderNodes with an elevation or + * translationZ.</p> + * + * <p>Any draw reordering will not be moved before this call. A typical usage of this might + * look something like: + * + * <pre class="prettyprint"> + * void draw(Canvas canvas) { + * // Draw any background content + * canvas.drawColor(backgroundColor); + * + * // Begin drawing that may be reordered based off of Z + * canvas.enableZ(); + * for (RenderNode child : children) { + * canvas.drawRenderNode(child); + * } + * // End drawing that may be reordered based off of Z + * canvas.disableZ(); + * + * // Draw any overlays + * canvas.drawText("I'm on top of everything!", 0, 0, paint); + * } + * </pre> + * </p> + * + * Note: This is not impacted by any {@link #save()} or {@link #restore()} calls as it is not + * considered to be part of the current matrix or clip. + * + * See {@link #disableZ()} + */ + public void enableZ() { + } + + /** + * Disables Z support, preventing any RenderNodes drawn after this point from being + * visually reordered or having shadows rendered. + * + * Note: This is not impacted by any {@link #save()} or {@link #restore()} calls as it is not + * considered to be part of the current matrix or clip. + * + * See {@link #enableZ()} + */ + public void disableZ() { + } /** * Return true if the device that the current layer draws into is opaque @@ -1877,6 +1936,51 @@ public class Canvas extends BaseCanvas { } /** + * Draws a double rounded rectangle using the specified paint. The resultant round rect + * will be filled in the area defined between the outer and inner rectangular bounds if + * the {@link Paint} configured with {@link Paint.Style#FILL}. + * Otherwise if {@link Paint.Style#STROKE} is used, then 2 rounded rect strokes will + * be drawn at the outer and inner rounded rectangles + * + * @param outer The outer rectangular bounds of the roundRect to be drawn + * @param outerRx The x-radius of the oval used to round the corners on the outer rectangle + * @param outerRy The y-radius of the oval used to round the corners on the outer rectangle + * @param inner The inner rectangular bounds of the roundRect to be drawn + * @param innerRx The x-radius of the oval used to round the corners on the inner rectangle + * @param innerRy The y-radius of the oval used to round the corners on the outer rectangle + * @param paint The paint used to draw the double roundRect + */ + @Override + public void drawDoubleRoundRect(@NonNull RectF outer, float outerRx, float outerRy, + @NonNull RectF inner, float innerRx, float innerRy, @NonNull Paint paint) { + super.drawDoubleRoundRect(outer, outerRx, outerRy, inner, innerRx, innerRy, paint); + } + + /** + * Draws a double rounded rectangle using the specified paint. The resultant round rect + * will be filled in the area defined between the outer and inner rectangular bounds if + * the {@link Paint} configured with {@link Paint.Style#FILL}. + * Otherwise if {@link Paint.Style#STROKE} is used, then 2 rounded rect strokes will + * be drawn at the outer and inner rounded rectangles + * + * @param outer The outer rectangular bounds of the roundRect to be drawn + * @param outerRadii Array of 8 float representing the x, y corner radii for top left, + * top right, bottom right, bottom left corners respectively on the outer + * rounded rectangle + * + * @param inner The inner rectangular bounds of the roundRect to be drawn + * @param innerRadii Array of 8 float representing the x, y corner radii for top left, + * top right, bottom right, bottom left corners respectively on the + * outer rounded rectangle + * @param paint The paint used to draw the double roundRect + */ + @Override + public void drawDoubleRoundRect(@NonNull RectF outer, float[] outerRadii, + @NonNull RectF inner, float[] innerRadii, @NonNull Paint paint) { + super.drawDoubleRoundRect(outer, outerRadii, inner, innerRadii, 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. * @@ -2065,4 +2169,17 @@ public class Canvas extends BaseCanvas { super.drawVertices(mode, vertexCount, verts, vertOffset, texs, texOffset, colors, colorOffset, indices, indexOffset, indexCount, paint); } + + /** + * Draws the given RenderNode. This is only supported in hardware rendering, which can be + * verified by asserting that {@link #isHardwareAccelerated()} is true. If + * {@link #isHardwareAccelerated()} is false then this throws an exception. + * + * See {@link RenderNode} for more information on what a RenderNode is and how to use it. + * + * @param renderNode The RenderNode to draw, must be non-null. + */ + public void drawRenderNode(@NonNull RenderNode renderNode) { + throw new IllegalArgumentException("Software rendering doesn't support drawRenderNode"); + } } diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java index 8fd02c0eeb59..2e1d81a294e9 100644 --- a/graphics/java/android/graphics/ColorSpace.java +++ b/graphics/java/android/graphics/ColorSpace.java @@ -239,7 +239,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -247,7 +247,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -302,7 +302,7 @@ public abstract class ColorSpace { * <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 \\ + * \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}\) @@ -312,7 +312,7 @@ public abstract class ColorSpace { * <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 \\ + * \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}\) @@ -367,7 +367,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -375,7 +375,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -402,7 +402,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -410,7 +410,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -464,7 +464,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -472,7 +472,7 @@ public abstract class ColorSpace { * <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.04045 \\ + * C_{linear} = \begin{cases}\frac{C_{DisplayP3}}{12.92} & C_{sRGB} \lt 0.04045 \\\ * \left( \frac{C_{DisplayP3} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \ge 0.04045 \end{cases} * \end{equation}\) * </td> @@ -499,7 +499,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -507,7 +507,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -534,7 +534,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -542,7 +542,7 @@ public abstract class ColorSpace { * <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 \\ + * 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> @@ -596,7 +596,7 @@ public abstract class ColorSpace { * <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_{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> @@ -604,7 +604,7 @@ public abstract class ColorSpace { * <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_{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> @@ -759,12 +759,12 @@ public abstract class ColorSpace { * * $$\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] \\ + * 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] \\ + * 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*}$$ * @@ -2028,7 +2028,7 @@ public abstract class ColorSpace { * <p>The EOTF is of the form:</p> * * \(\begin{equation} - * Y = \begin{cases}c X + f & X \lt d \\ + * Y = \begin{cases}c X + f & X \lt d \\\ * \left( a X + b \right) ^{g} + e & X \ge d \end{cases} * \end{equation}\) * @@ -2066,7 +2066,7 @@ public abstract class ColorSpace { * <p>The EOTF is of the form:</p> * * \(\begin{equation} - * Y = \begin{cases}c X & X \lt d \\ + * Y = \begin{cases}c X & X \lt d \\\ * \left( a X + b \right) ^{g} & X \ge d \end{cases} * \end{equation}\) * @@ -2525,9 +2525,7 @@ public abstract class ColorSpace { 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); + mTransferParameters = new TransferParameters(1.0, 0.0, 0.0, 0.0, gamma); } /** diff --git a/graphics/java/android/graphics/FontFamily.java b/graphics/java/android/graphics/FontFamily.java index 06469153c3ac..229923ca8202 100644 --- a/graphics/java/android/graphics/FontFamily.java +++ b/graphics/java/android/graphics/FontFamily.java @@ -64,6 +64,7 @@ public class FontFamily { mNativeBuilderCleaner = sBuilderRegistry.registerNativeAllocation(this, mBuilderPtr); } + @UnsupportedAppUsage public FontFamily(@Nullable String[] langs, int variant) { final String langsString; if (langs == null || langs.length == 0) { diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java index e3e8380716f3..21cc3757a40e 100644 --- a/graphics/java/android/graphics/FontListParser.java +++ b/graphics/java/android/graphics/FontListParser.java @@ -40,17 +40,25 @@ public class FontListParser { /* Parse fallback list (no names) */ @UnsupportedAppUsage public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException { + return parse(in, "/system/fonts"); + } + + /** + * Parse the fonts.xml + */ + public static FontConfig parse(InputStream in, String fontDir) + throws XmlPullParserException, IOException { try { XmlPullParser parser = Xml.newPullParser(); parser.setInput(in, null); parser.nextTag(); - return readFamilies(parser); + return readFamilies(parser, fontDir); } finally { in.close(); } } - private static FontConfig readFamilies(XmlPullParser parser) + private static FontConfig readFamilies(XmlPullParser parser, String fontDir) throws XmlPullParserException, IOException { List<FontConfig.Family> families = new ArrayList<>(); List<FontConfig.Alias> aliases = new ArrayList<>(); @@ -60,7 +68,7 @@ public class FontListParser { if (parser.getEventType() != XmlPullParser.START_TAG) continue; String tag = parser.getName(); if (tag.equals("family")) { - families.add(readFamily(parser)); + families.add(readFamily(parser, fontDir)); } else if (tag.equals("alias")) { aliases.add(readAlias(parser)); } else { @@ -71,18 +79,20 @@ public class FontListParser { aliases.toArray(new FontConfig.Alias[aliases.size()])); } - private static FontConfig.Family readFamily(XmlPullParser parser) + /** + * Reads a family element + */ + public static FontConfig.Family readFamily(XmlPullParser parser, String fontDir) 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 lang = parser.getAttributeValue("", "lang"); 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)); + fonts.add(readFont(parser, fontDir)); } else { skip(parser); } @@ -95,7 +105,7 @@ public class FontListParser { intVariant = FontConfig.Family.VARIANT_ELEGANT; } } - return new FontConfig.Family(name, fonts.toArray(new FontConfig.Font[fonts.size()]), langs, + return new FontConfig.Family(name, fonts.toArray(new FontConfig.Font[fonts.size()]), lang, intVariant); } @@ -103,7 +113,7 @@ public class FontListParser { private static final Pattern FILENAME_WHITESPACE_PATTERN = Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$"); - private static FontConfig.Font readFont(XmlPullParser parser) + private static FontConfig.Font readFont(XmlPullParser parser, String fontDir) throws XmlPullParserException, IOException { String indexStr = parser.getAttributeValue(null, "index"); int index = indexStr == null ? 0 : Integer.parseInt(indexStr); @@ -126,8 +136,8 @@ public class FontListParser { } } String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll(""); - return new FontConfig.Font(sanitizedName, index, - axes.toArray(new FontVariationAxis[axes.size()]), weight, isItalic, fallbackFor); + return new FontConfig.Font(fontDir + sanitizedName, index, axes.toArray( + new FontVariationAxis[axes.size()]), weight, isItalic, fallbackFor); } private static FontVariationAxis readAxis(XmlPullParser parser) @@ -138,7 +148,10 @@ public class FontListParser { return new FontVariationAxis(tagStr, Float.parseFloat(styleValueStr)); } - private static FontConfig.Alias readAlias(XmlPullParser parser) + /** + * Reads alias elements + */ + public static FontConfig.Alias readAlias(XmlPullParser parser) throws XmlPullParserException, IOException { String name = parser.getAttributeValue(null, "name"); String toName = parser.getAttributeValue(null, "to"); @@ -153,7 +166,10 @@ public class FontListParser { return new FontConfig.Alias(name, toName, weight); } - private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { + /** + * Skip until next element + */ + public static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { int depth = 1; while (depth > 0) { switch (parser.next()) { diff --git a/graphics/java/android/graphics/FrameInfo.java b/graphics/java/android/graphics/FrameInfo.java new file mode 100644 index 000000000000..42a5cc43eaa0 --- /dev/null +++ b/graphics/java/android/graphics/FrameInfo.java @@ -0,0 +1,128 @@ +/* + * 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 android.annotation.LongDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Class that contains all the timing information for the current frame. This + * is used in conjunction with the hardware renderer to provide + * continous-monitoring jank events + * + * All times in nanoseconds from CLOCK_MONOTONIC/System.nanoTime() + * + * To minimize overhead from System.nanoTime() calls we infer durations of + * things by knowing the ordering of the events. For example, to know how + * long layout & measure took it's displayListRecordStart - performTraversalsStart. + * + * These constants must be kept in sync with FrameInfo.h in libhwui and are + * used for indexing into AttachInfo's frameInfo long[], which is intended + * to be quick to pass down to native via JNI, hence a pre-packed format + * + * @hide + */ +public final class FrameInfo { + + public long[] frameInfo = new long[9]; + + // Various flags set to provide extra metadata about the current frame + private static final int FLAGS = 0; + + // Is this the first-draw following a window layout? + public static final long FLAG_WINDOW_LAYOUT_CHANGED = 1; + + // A renderer associated with just a Surface, not with a ViewRootImpl instance. + public static final long FLAG_SURFACE_CANVAS = 1 << 2; + + @LongDef(flag = true, value = { + FLAG_WINDOW_LAYOUT_CHANGED, FLAG_SURFACE_CANVAS }) + @Retention(RetentionPolicy.SOURCE) + public @interface FrameInfoFlags {} + + // The intended vsync time, unadjusted by jitter + private static final int INTENDED_VSYNC = 1; + + // Jitter-adjusted vsync time, this is what was used as input into the + // animation & drawing system + private static final int VSYNC = 2; + + // The time of the oldest input event + private static final int OLDEST_INPUT_EVENT = 3; + + // The time of the newest input event + private static final int NEWEST_INPUT_EVENT = 4; + + // When input event handling started + private static final int HANDLE_INPUT_START = 5; + + // When animation evaluations started + private static final int ANIMATION_START = 6; + + // When ViewRootImpl#performTraversals() started + private static final int PERFORM_TRAVERSALS_START = 7; + + // When View:draw() started + private static final int DRAW_START = 8; + + /** checkstyle */ + public void setVsync(long intendedVsync, long usedVsync) { + frameInfo[INTENDED_VSYNC] = intendedVsync; + frameInfo[VSYNC] = usedVsync; + frameInfo[OLDEST_INPUT_EVENT] = Long.MAX_VALUE; + frameInfo[NEWEST_INPUT_EVENT] = 0; + frameInfo[FLAGS] = 0; + } + + /** checkstyle */ + public void updateInputEventTime(long inputEventTime, long inputEventOldestTime) { + if (inputEventOldestTime < frameInfo[OLDEST_INPUT_EVENT]) { + frameInfo[OLDEST_INPUT_EVENT] = inputEventOldestTime; + } + if (inputEventTime > frameInfo[NEWEST_INPUT_EVENT]) { + frameInfo[NEWEST_INPUT_EVENT] = inputEventTime; + } + } + + /** checkstyle */ + public void markInputHandlingStart() { + frameInfo[HANDLE_INPUT_START] = System.nanoTime(); + } + + /** checkstyle */ + public void markAnimationsStart() { + frameInfo[ANIMATION_START] = System.nanoTime(); + } + + /** checkstyle */ + public void markPerformTraversalsStart() { + frameInfo[PERFORM_TRAVERSALS_START] = System.nanoTime(); + } + + /** checkstyle */ + public void markDrawStart() { + frameInfo[DRAW_START] = System.nanoTime(); + } + + /** checkstyle */ + public void addFlags(@FrameInfoFlags long flags) { + frameInfo[FLAGS] |= flags; + } + +} diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java new file mode 100644 index 000000000000..e4020554786b --- /dev/null +++ b/graphics/java/android/graphics/HardwareRenderer.java @@ -0,0 +1,1030 @@ +/* + * Copyright (C) 2018 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.annotation.Nullable; +import android.app.Activity; +import android.app.ActivityManager; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; +import android.view.FrameMetricsObserver; +import android.view.IGraphicsStats; +import android.view.IGraphicsStatsCallback; +import android.view.NativeVectorDrawableAnimator; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.TextureLayer; + +import com.android.internal.util.VirtualRefBasePtr; + +import java.io.File; +import java.io.FileDescriptor; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import sun.misc.Cleaner; + +/** + * <p>Creates an instance of a hardware-accelerated renderer. This is used to render a scene built + * from {@link RenderNode}'s to an output {@link android.view.Surface}. There can be as many + * HardwareRenderer instances as desired.</p> + * + * <h3>Threading</h3> + * <p>HardwareRenderer is not thread safe. An instance of a HardwareRenderer must only be + * created & used from a single thread. It does not matter what thread is used, however + * it must have a {@link android.os.Looper}. Multiple instances do not have to share the same + * thread, although they can.</p> + * + * <h3>Resources & lifecycle</h3> + * <p>All HardwareRenderer instances share a common render thread. The render thread contains + * the GPU context & resources necessary to do GPU-accelerated rendering. As such, the first + * HardwareRenderer created comes with the cost of also creating the associated GPU contexts, + * however each incremental HardwareRenderer thereafter is fairly cheap. The expected usage + * is to have a HardwareRenderer instance for every active {@link Surface}. For example + * when an Activity shows a Dialog the system internally will use 2 hardware renderers, both + * of which may be drawing at the same time.</p> + * <p>NOTE: Due to the shared, cooperative nature of the render thread it is critical that + * any {@link Surface} used must have a prompt, reliable consuming side. System-provided + * consumers such as {@link android.view.SurfaceView}, + * {@link android.view.Window#takeSurface(SurfaceHolder.Callback2)}, + * or {@link android.view.TextureView} all fit this requirement. However if custom consumers + * are used such as when using {@link SurfaceTexture} or {@link android.media.ImageReader} + * it is the app's responsibility to ensure that they consume updates promptly and rapidly. + * Failure to do so will cause the render thread to stall on that surface, blocking all + * HardwareRenderer instances.</p> + * + * @hide + */ +public class HardwareRenderer { + private static final String LOG_TAG = "HardwareRenderer"; + + // Keep in sync with DrawFrameTask.h SYNC_* flags + /** + * Nothing interesting to report. Sync & draw kicked off + */ + public static final int SYNC_OK = 0; + + /** + * The renderer is requesting a redraw. This can occur if there's an animation that's running + * in the RenderNode tree and the hardware renderer is unable to self-animate. + * + * If this is returned from syncAndDrawFrame the expectation is that syncAndDrawFrame + * will be called again on the next vsync signal. + */ + public static final int SYNC_REDRAW_REQUESTED = 1 << 0; + + /** + * The hardware renderer no longer has a valid {@link android.view.Surface} to render to. + * This can happen if {@link Surface#destroy()} was called. The user should no longer + * attempt to call syncAndDrawFrame until a new surface has been provided by calling + * setSurface. + * + * Spoiler: the reward is GPU-accelerated drawing, better find that Surface! + */ + public static final int SYNC_LOST_SURFACE_REWARD_IF_FOUND = 1 << 1; + + /** + * The hardware renderer has been set to a "stopped" state. If this is returned then the + * rendering content has been synced, however a frame was not produced. + */ + public static final int SYNC_CONTEXT_IS_STOPPED = 1 << 2; + + /** + * The content was synced but the renderer has declined to produce a frame in this vsync + * interval. This can happen if a frame was already drawn in this vsync or if the renderer + * is outrunning the frame consumer. The renderer will internally re-schedule itself + * to render a frame in the next vsync signal, so the caller does not need to do anything + * in response to this signal. + */ + public static final int SYNC_FRAME_DROPPED = 1 << 3; + + @IntDef(value = { + SYNC_OK, SYNC_REDRAW_REQUESTED, SYNC_LOST_SURFACE_REWARD_IF_FOUND, + SYNC_CONTEXT_IS_STOPPED, SYNC_FRAME_DROPPED}) + @Retention(RetentionPolicy.SOURCE) + public @interface SyncAndDrawResult { + } + + /** @hide */ + public static final int FLAG_DUMP_FRAMESTATS = 1 << 0; + /** @hide */ + public static final int FLAG_DUMP_RESET = 1 << 1; + /** @hide */ + public static final int FLAG_DUMP_ALL = FLAG_DUMP_FRAMESTATS; + + /** @hide */ + @IntDef(flag = true, prefix = {"FLAG_DUMP_"}, value = { + FLAG_DUMP_FRAMESTATS, + FLAG_DUMP_RESET + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DumpFlags { + } + + /** + * Name of the file that holds the shaders cache. + */ + private static final String CACHE_PATH_SHADERS = "com.android.opengl.shaders_cache"; + private static final String CACHE_PATH_SKIASHADERS = "com.android.skia.shaders_cache"; + + private final long mNativeProxy; + /** @hide */ + protected RenderNode mRootNode; + private boolean mOpaque = true; + private boolean mForceDark = false; + private FrameInfo mScratchInfo; + + /** + * Creates a new instance of a HardwareRenderer. The HardwareRenderer will default + * to opaque with no light source configured. + */ + public HardwareRenderer() { + mRootNode = RenderNode.adopt(nCreateRootRenderNode()); + mRootNode.setClipToBounds(false); + mNativeProxy = nCreateProxy(!mOpaque, mRootNode.mNativeRenderNode); + if (mNativeProxy == 0) { + throw new OutOfMemoryError("Unable to create hardware renderer"); + } + Cleaner.create(this, new DestroyContextRunnable(mNativeProxy)); + ProcessInitializer.sInstance.init(mNativeProxy); + } + + /** + * Destroys the rendering context of this HardwareRenderer. This destroys the resources + * associated with this renderer and releases the currently set {@link Surface}. + * + * The renderer may be restored from this state by setting a new {@link Surface}, setting + * new rendering content with {@link #setContentRoot(RenderNode)}, and resuming + * rendering with {@link #syncAndDrawFrame(long)}. + * + * It is suggested to call this in response to callbacks such as + * {@link android.view.SurfaceHolder.Callback#surfaceDestroyed(SurfaceHolder)}. + * + * Note that if there are any outstanding frame commit callbacks they may end up never being + * invoked if the frame was deferred to a later vsync. + */ + public void destroy() { + nDestroy(mNativeProxy, mRootNode.mNativeRenderNode); + } + + /** + * Sets a name for this renderer. This is used to identify this renderer instance + * when reporting debug information such as the per-window frame time metrics + * reported by 'adb shell dumpsys gfxinfo [package] framestats' + * + * @param name The debug name to use for this HardwareRenderer instance + */ + public void setName(String name) { + nSetName(mNativeProxy, name); + } + + /** + * Sets the center of the light source. The light source point controls the directionality + * and shape of shadows rendered by RenderNode Z & elevation. + * + * The platform's recommendation is to set lightX to 'displayWidth / 2f - windowLeft', set + * lightY to 0 - windowTop, lightZ set to 600dp, and lightRadius to 800dp. + * + * The light source should be setup both as part of initial configuration, and whenever + * the window moves to ensure the light source stays anchored in display space instead + * of in window space. + * + * This must be set at least once along with {@link #setLightSourceAlpha(float, float)} + * before shadows will work. + * + * @param lightX The X position of the light source + * @param lightY The Y position of the light source + * @param lightZ The Z position of the light source. Must be >= 0. + * @param lightRadius The radius of the light source. Smaller radius will have sharper edges, + * larger radius will have softer shadows. + */ + public void setLightSourceGeometry(float lightX, float lightY, float lightZ, + float lightRadius) { + validateFinite(lightX, "lightX"); + validateFinite(lightY, "lightY"); + validatePositive(lightZ, "lightZ"); + validatePositive(lightRadius, "lightRadius"); + nSetLightGeometry(mNativeProxy, lightX, lightY, lightZ, lightRadius); + } + + /** + * Configures the ambient & spot shadow alphas. This is the alpha used when the shadow + * has max alpha, and ramps down from the values provided to zero. + * + * These values are typically provided by the current theme, see + * {@link android.R.attr#spotShadowAlpha} and {@link android.R.attr#ambientShadowAlpha}. + * + * This must be set at least once along with + * {@link #setLightSourceGeometry(float, float, float, float)} before shadows will work. + * + * @param ambientShadowAlpha The alpha for the ambient shadow. If unsure, a reasonable default + * is 0.039f. + * @param spotShadowAlpha The alpha for the spot shadow. If unsure, a reasonable default is + * 0.19f. + */ + public void setLightSourceAlpha(@FloatRange(from = 0.0f, to = 1.0f) float ambientShadowAlpha, + @FloatRange(from = 0.0f, to = 1.0f) float spotShadowAlpha) { + validateAlpha(ambientShadowAlpha, "ambientShadowAlpha"); + validateAlpha(spotShadowAlpha, "spotShadowAlpha"); + nSetLightAlpha(mNativeProxy, ambientShadowAlpha, spotShadowAlpha); + } + + /** + * Sets the content root to render. It is not necessary to call this whenever the content + * recording changes. Any mutations to the RenderNode content, or any of the RenderNode's + * contained within the content node, will be applied whenever {@link #syncAndDrawFrame(long)} + * is called. + * + * @param content The content to set as the root RenderNode. If null the content root is removed + * and the renderer will draw nothing. + */ + public void setContentRoot(@Nullable RenderNode content) { + RecordingCanvas canvas = mRootNode.startRecording(); + if (content != null) { + canvas.drawRenderNode(content); + } + mRootNode.endRecording(); + } + + /** + * <p>The surface to render into. The surface is assumed to be associated with the display and + * as such is still driven by vsync signals such as those from + * {@link android.view.Choreographer} and that it has a native refresh rate matching that of + * the display's (typically 60hz).</p> + * + * <p>NOTE: Due to the shared, cooperative nature of the render thread it is critical that + * any {@link Surface} used must have a prompt, reliable consuming side. System-provided + * consumers such as {@link android.view.SurfaceView}, + * {@link android.view.Window#takeSurface(SurfaceHolder.Callback2)}, + * or {@link android.view.TextureView} all fit this requirement. However if custom consumers + * are used such as when using {@link SurfaceTexture} or {@link android.media.ImageReader} + * it is the app's responsibility to ensure that they consume updates promptly and rapidly. + * Failure to do so will cause the render thread to stall on that surface, blocking all + * HardwareRenderer instances.</p> + * + * @param surface The surface to render into. If null then rendering will be stopped. If + * non-null then {@link Surface#isValid()} must be true. + */ + public void setSurface(@Nullable Surface surface) { + if (surface != null && !surface.isValid()) { + throw new IllegalArgumentException("Surface is invalid. surface.isValid() == false."); + } + nSetSurface(mNativeProxy, surface); + } + + /** + * Syncs the RenderNode tree to the render thread and requests a frame to be drawn. + * + * @hide + */ + @SyncAndDrawResult + public int syncAndDrawFrame(@NonNull FrameInfo frameInfo) { + return nSyncAndDrawFrame(mNativeProxy, frameInfo.frameInfo, frameInfo.frameInfo.length); + } + + /** + * Syncs the RenderNode tree to the render thread and requests a frame to be drawn. + * + * @param vsyncTime The vsync timestamp for this frame. Typically this comes from + * {@link android.view.Choreographer.FrameCallback}. Must be set and be valid + * as the renderer uses this time internally to drive animations. + * @return The result of the sync operation. See {@link SyncAndDrawResult}. + */ + @SyncAndDrawResult + public int syncAndDrawFrame(long vsyncTime) { + if (mScratchInfo == null) { + mScratchInfo = new FrameInfo(); + } + mScratchInfo.setVsync(vsyncTime, vsyncTime); + mScratchInfo.addFlags(FrameInfo.FLAG_SURFACE_CANVAS); + return syncAndDrawFrame(mScratchInfo); + } + + /** + * Syncs the RenderNode tree to the render thread and requests a frame to be drawn. + * frameCommitCallback callback will be invoked when the current rendering content has been + * rendered into a frame and submitted to the swap chain. + * + * @param vsyncTime The vsync timestamp for this frame. Typically this comes from + * {@link android.view.Choreographer.FrameCallback}. Must be set and + * be valid as the renderer uses this time internally to drive + * animations. + * @param frameCommitCallback The callback to invoke when the frame content has been drawn. + * Will be invoked on the current {@link android.os.Looper} thread. + * @return The result of the sync operation. See {@link SyncAndDrawResult}. + */ + @SyncAndDrawResult + public int syncAndDrawFrame(long vsyncTime, + @Nullable Runnable frameCommitCallback) { + if (frameCommitCallback != null) { + setFrameCompleteCallback(frameNr -> frameCommitCallback.run()); + } + return syncAndDrawFrame(vsyncTime); + } + + /** + * Suspends any current rendering into the surface but do not do any destruction. This + * is useful to temporarily suspend using the active Surface in order to do any Surface + * mutations necessary. + * + * Any subsequent draws will override the pause, resuming normal operation. + * + * @return true if there was an outstanding render request, false otherwise. If this is true + * the caller should ensure that {@link #syncAndDrawFrame(long)} is called at the soonest + * possible time to resume normal operation. + * + * TODO Should this be exposed? ViewRootImpl needs it because it destroys the old + * Surface before getting a new one. However things like SurfaceView will ensure that + * the old surface remains un-destroyed until after a new frame has been produced with + * the new surface. + * @hide + */ + public boolean pause() { + return nPause(mNativeProxy); + } + + /** + * Hard stops rendering into the surface. If the renderer is stopped it will + * block any attempt to render. Calls to {@link #syncAndDrawFrame(long)} will still + * sync over the latest rendering content, however they will not render and instead + * {@link #SYNC_CONTEXT_IS_STOPPED} will be returned. + * + * If false is passed then rendering will resume as normal. Any pending rendering requests + * will produce a new frame at the next vsync signal. + * + * This is useful in combination with lifecycle events such as {@link Activity#onStop()} + * and {@link Activity#onStart()}. + * + * @param stopped true to stop all rendering, false to resume + */ + public void setStopped(boolean stopped) { + nSetStopped(mNativeProxy, stopped); + } + + /** + * Destroys all hardware rendering resources associated with the current rendering content. + * This includes releasing a reference to the current content root RenderNode. It will + * therefore be necessary to call {@link #setContentRoot(RenderNode)} in order to resume + * rendering after calling this. + * + * It is recommended, but not necessary, to use this in combination with lifecycle events + * such as {@link Activity#onStop()} and {@link Activity#onStart()} or in response to + * {@link android.content.ComponentCallbacks2#onTrimMemory(int)} signals such as + * {@link android.content.ComponentCallbacks2#TRIM_MEMORY_UI_HIDDEN} + * + * See also {@link #setStopped(boolean)} + */ + public void destroyHardwareResources() { + nDestroyHardwareResources(mNativeProxy); + } + + /** + * Whether or not the force-dark feature should be used for this renderer. + */ + public boolean setForceDark(boolean enable) { + if (mForceDark != enable) { + mForceDark = enable; + nSetForceDark(mNativeProxy, enable); + return true; + } + return false; + } + + /** + * Allocate buffers ahead of time to avoid allocation delays during rendering. + * + * Typically a Surface will allocate buffers lazily. This is usually fine and reduces the + * memory usage of Surfaces that render rarely or never hit triple buffering. However + * for UI it can result in a slight bit of jank on first launch. This hint will + * tell the HardwareRenderer that now is a good time to allocate the 3 buffers + * necessary for typical rendering. + * + * Must be called after a {@link Surface} has been set. + */ + public void allocateBuffers() { + nAllocateBuffers(mNativeProxy); + } + + /** + * Notifies the hardware renderer that a call to {@link #syncAndDrawFrame(long)} will + * be coming soon. This is used to help schedule when RenderThread-driven animations will + * happen as the renderer wants to avoid producing more than one frame per vsync signal. + */ + public void notifyFramePending() { + nNotifyFramePending(mNativeProxy); + } + + /** + * Change the HardwareRenderer's opacity. Will take effect on the next frame produced. + * + * If the renderer is set to opaque it is the app's responsibility to ensure that the + * content renders to every pixel of the Surface, otherwise corruption may result. Note that + * this includes ensuring that the first draw of any given pixel does not attempt to blend + * against the destination. If this is false then the hardware renderer will clear to + * transparent at the start of every frame. + * + * @param opaque true if the content rendered is opaque, false if the renderer should clear + * to transparent before rendering + */ + public void setOpaque(boolean opaque) { + if (mOpaque != opaque) { + mOpaque = opaque; + nSetOpaque(mNativeProxy, mOpaque); + } + } + + /** + * Whether or not the renderer is set to be opaque. See {@link #setOpaque(boolean)} + * + * @return true if the renderer is opaque, false otherwise + */ + public boolean isOpaque() { + return mOpaque; + } + + /** @hide */ + public void setFrameCompleteCallback(FrameCompleteCallback callback) { + nSetFrameCompleteCallback(mNativeProxy, callback); + } + + /** + * TODO: Public API this? + * + * @hide + */ + public void addFrameMetricsObserver(FrameMetricsObserver observer) { + long nativeObserver = nAddFrameMetricsObserver(mNativeProxy, observer); + observer.mNative = new VirtualRefBasePtr(nativeObserver); + } + + /** + * TODO: Public API this? + * + * @hide + */ + public void removeFrameMetricsObserver(FrameMetricsObserver observer) { + nRemoveFrameMetricsObserver(mNativeProxy, observer.mNative.get()); + observer.mNative = null; + } + + /** + * Enable/disable wide gamut rendering on this renderer. Whether or not the actual rendering + * will be wide gamut depends on the hardware support for such rendering. + * + * @param wideGamut true if this renderer should render in wide gamut, false if it should + * render in sRGB + * TODO: Figure out color... + * @hide + */ + public void setWideGamut(boolean wideGamut) { + nSetWideGamut(mNativeProxy, wideGamut); + } + + /** + * Blocks until all previously queued work has completed. + * + * TODO: Only used for draw finished listeners, but the FrameCompleteCallback does that + * better + * + * @hide + */ + public void fence() { + nFence(mNativeProxy); + } + + /** @hide */ + public void registerAnimatingRenderNode(RenderNode animator) { + nRegisterAnimatingRenderNode(mRootNode.mNativeRenderNode, animator.mNativeRenderNode); + } + + /** @hide */ + public void registerVectorDrawableAnimator(NativeVectorDrawableAnimator animator) { + nRegisterVectorDrawableAnimator(mRootNode.mNativeRenderNode, + animator.getAnimatorNativePtr()); + } + + /** + * Prevents any further drawing until {@link #syncAndDrawFrame(long)} is called. + * This is a signal that the contents of the RenderNode tree are no longer safe to play back. + * In practice this usually means that there are Functor pointers in the + * display list that are no longer valid. + * + * TODO: Can we get webview off of this? + * + * @hide + */ + public void stopDrawing() { + nStopDrawing(mNativeProxy); + } + + /** + * Creates a new hardware layer. A hardware layer built by calling this + * method will be treated as a texture layer, instead of as a render target. + * + * @return A hardware layer + * @hide + */ + public TextureLayer createTextureLayer() { + long layer = nCreateTextureLayer(mNativeProxy); + return TextureLayer.adoptTextureLayer(this, layer); + } + + /** + * Detaches the layer's surface texture from the GL context and releases + * the texture id + * + * @hide + */ + public void detachSurfaceTexture(long hardwareLayer) { + nDetachSurfaceTexture(mNativeProxy, hardwareLayer); + } + + + /** @hide */ + public void buildLayer(RenderNode node) { + if (node.hasDisplayList()) { + nBuildLayer(mNativeProxy, node.mNativeRenderNode); + } + } + + /** @hide */ + public boolean copyLayerInto(final TextureLayer layer, final Bitmap bitmap) { + return nCopyLayerInto(mNativeProxy, + layer.getDeferredLayerUpdater(), bitmap); + } + + /** + * Indicates that the specified hardware layer needs to be updated + * as soon as possible. + * + * @param layer The hardware layer that needs an update + * @hide + */ + public void pushLayerUpdate(TextureLayer layer) { + nPushLayerUpdate(mNativeProxy, layer.getDeferredLayerUpdater()); + } + + /** + * Tells the HardwareRenderer that the layer is destroyed. The renderer + * should remove the layer from any update queues. + * + * @hide + */ + public void onLayerDestroyed(TextureLayer layer) { + nCancelLayerUpdate(mNativeProxy, layer.getDeferredLayerUpdater()); + } + + /** @hide */ + public void setFrameCallback(FrameDrawingCallback callback) { + nSetFrameCallback(mNativeProxy, callback); + } + + /** + * Adds a rendernode to the renderer which can be drawn and changed asynchronously to the + * rendernode of the UI thread. + * + * @param node The node to add. + * @param placeFront If true, the render node will be placed in front of the content node, + * otherwise behind the content node. + * @hide + */ + public void addRenderNode(RenderNode node, boolean placeFront) { + nAddRenderNode(mNativeProxy, node.mNativeRenderNode, placeFront); + } + + /** + * Only especially added render nodes can be removed. + * + * @param node The node which was added via addRenderNode which should get removed again. + * @hide + */ + public void removeRenderNode(RenderNode node) { + nRemoveRenderNode(mNativeProxy, node.mNativeRenderNode); + } + + /** + * Draws a particular render node. If the node is not the content node, only the additional + * nodes will get drawn and the content remains untouched. + * + * @param node The node to be drawn. + * @hide + */ + public void drawRenderNode(RenderNode node) { + nDrawRenderNode(mNativeProxy, node.mNativeRenderNode); + } + + /** + * Loads system properties used by the renderer. This method is invoked + * whenever system properties are modified. Implementations can use this + * to trigger live updates of the renderer based on properties. + * + * @return True if a property has changed. + * @hide + */ + public boolean loadSystemProperties() { + return nLoadSystemProperties(mNativeProxy); + } + + /** + * @hide + */ + public void dumpProfileInfo(FileDescriptor fd, @DumpFlags int dumpFlags) { + nDumpProfileInfo(mNativeProxy, fd, dumpFlags); + } + + /** + * To avoid unnecessary overdrawing of the main content all additionally passed render nodes + * will be prevented to overdraw this area. It will be synchronized with the draw call. + * This should be updated in the content view's draw call. + * + * @param left The left side of the protected bounds. + * @param top The top side of the protected bounds. + * @param right The right side of the protected bounds. + * @param bottom The bottom side of the protected bounds. + * @hide + */ + public void setContentDrawBounds(int left, int top, int right, int bottom) { + nSetContentDrawBounds(mNativeProxy, left, top, right, bottom); + } + + /** + * Interface used to receive callbacks when a frame is being drawn. + * + * @hide + */ + public interface FrameDrawingCallback { + /** + * Invoked during a frame drawing. + * + * @param frame The id of the frame being drawn. + */ + void onFrameDraw(long frame); + } + + /** + * Interface used to be notified when a frame has finished rendering + * + * @hide + */ + public interface FrameCompleteCallback { + /** + * Invoked after a frame draw + * + * @param frameNr The id of the frame that was drawn. + */ + void onFrameComplete(long frameNr); + } + + private static void validateAlpha(float alpha, String argumentName) { + if (!(alpha >= 0.0f && alpha <= 1.0f)) { + throw new IllegalArgumentException(argumentName + " must be a valid alpha, " + + alpha + " is not in the range of 0.0f to 1.0f"); + } + } + + private static void validatePositive(float f, String argumentName) { + if (!(Float.isFinite(f) && f >= 0.0f)) { + throw new IllegalArgumentException(argumentName + + " must be a finite positive, given=" + f); + } + } + + private static void validateFinite(float f, String argumentName) { + if (!Float.isFinite(f)) { + throw new IllegalArgumentException(argumentName + " must be finite, given=" + f); + } + } + + /** @hide */ + public static void invokeFunctor(long functor, boolean waitForCompletion) { + nInvokeFunctor(functor, waitForCompletion); + } + + /** + * b/68769804: For low FPS experiments. + * + * @hide + */ + public static void setFPSDivisor(int divisor) { + nHackySetRTAnimationsEnabled(divisor <= 1); + } + + /** + * Changes the OpenGL context priority if IMG_context_priority extension is available. Must be + * called before any OpenGL context is created. + * + * @param priority The priority to use. Must be one of EGL_CONTEXT_PRIORITY_* values. + * @hide + */ + public static void setContextPriority(int priority) { + nSetContextPriority(priority); + } + + /** + * Sets whether or not high contrast text rendering is enabled. The setting is global + * but only affects content rendered after the change is made. + * + * @hide + */ + public static void setHighContrastText(boolean highContrastText) { + nSetHighContrastText(highContrastText); + } + + /** + * If set RenderThread will avoid doing any IPC using instead a fake vsync & DisplayInfo source + * + * @hide + */ + public static void setIsolatedProcess(boolean isIsolated) { + nSetIsolatedProcess(isIsolated); + } + + /** + * If set extra graphics debugging abilities will be enabled such as dumping skp + * + * @hide + */ + public static void setDebuggingEnabled(boolean enable) { + nSetDebuggingEnabled(enable); + } + + /** @hide */ + public static int copySurfaceInto(Surface surface, Rect srcRect, Bitmap bitmap) { + if (srcRect == null) { + // Empty rect means entire surface + return nCopySurfaceInto(surface, 0, 0, 0, 0, bitmap); + } else { + return nCopySurfaceInto(surface, srcRect.left, srcRect.top, + srcRect.right, srcRect.bottom, bitmap); + } + } + + /** + * Creates a {@link android.graphics.Bitmap.Config#HARDWARE} bitmap from the given + * RenderNode. Note that the RenderNode should be created as a root node (so x/y of 0,0), and + * not the RenderNode from a View. + * + * @hide + **/ + public static Bitmap createHardwareBitmap(RenderNode node, int width, int height) { + return nCreateHardwareBitmap(node.mNativeRenderNode, width, height); + } + + /** + * Invoke this method when the system is running out of memory. This + * method will attempt to recover as much memory as possible, based on + * the specified hint. + * + * @param level Hint about the amount of memory that should be trimmed, + * see {@link android.content.ComponentCallbacks} + * @hide + */ + public static void trimMemory(int level) { + nTrimMemory(level); + } + + /** @hide */ + public static void overrideProperty(@NonNull String name, @NonNull String value) { + if (name == null || value == null) { + throw new IllegalArgumentException("name and value must be non-null"); + } + nOverrideProperty(name, value); + } + + /** + * Sets the directory to use as a persistent storage for threaded rendering + * resources. + * + * @param cacheDir A directory the current process can write to + * @hide + */ + public static void setupDiskCache(File cacheDir) { + setupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath(), + new File(cacheDir, CACHE_PATH_SKIASHADERS).getAbsolutePath()); + } + + /** @hide */ + public static void setPackageName(String packageName) { + ProcessInitializer.sInstance.setPackageName(packageName); + } + + private static final class DestroyContextRunnable implements Runnable { + private final long mNativeInstance; + + DestroyContextRunnable(long nativeInstance) { + mNativeInstance = nativeInstance; + } + + @Override + public void run() { + nDeleteProxy(mNativeInstance); + } + } + + private static class ProcessInitializer { + static ProcessInitializer sInstance = new ProcessInitializer(); + + private boolean mInitialized = false; + + private String mPackageName; + private IGraphicsStats mGraphicsStatsService; + private IGraphicsStatsCallback mGraphicsStatsCallback = new IGraphicsStatsCallback.Stub() { + @Override + public void onRotateGraphicsStatsBuffer() throws RemoteException { + rotateBuffer(); + } + }; + + private ProcessInitializer() { + } + + synchronized void setPackageName(String name) { + if (mInitialized) return; + mPackageName = name; + } + + synchronized void init(long renderProxy) { + if (mInitialized) return; + mInitialized = true; + + initSched(renderProxy); + initGraphicsStats(); + } + + private void initSched(long renderProxy) { + try { + int tid = nGetRenderThreadTid(renderProxy); + ActivityManager.getService().setRenderThread(tid); + } catch (Throwable t) { + Log.w(LOG_TAG, "Failed to set scheduler for RenderThread", t); + } + } + + private void initGraphicsStats() { + if (mPackageName == null) return; + + try { + IBinder binder = ServiceManager.getService("graphicsstats"); + if (binder == null) return; + mGraphicsStatsService = IGraphicsStats.Stub.asInterface(binder); + requestBuffer(); + } catch (Throwable t) { + Log.w(LOG_TAG, "Could not acquire gfx stats buffer", t); + } + } + + private void rotateBuffer() { + nRotateProcessStatsBuffer(); + requestBuffer(); + } + + private void requestBuffer() { + try { + ParcelFileDescriptor pfd = mGraphicsStatsService + .requestBufferForProcess(mPackageName, mGraphicsStatsCallback); + nSetProcessStatsBuffer(pfd.getFd()); + pfd.close(); + } catch (Throwable t) { + Log.w(LOG_TAG, "Could not acquire gfx stats buffer", t); + } + } + } + + /** + * @hide + */ + public static native void disableVsync(); + + /** @hide */ + protected static native void setupShadersDiskCache(String cacheFile, String skiaCacheFile); + + private static native void nRotateProcessStatsBuffer(); + + private static native void nSetProcessStatsBuffer(int fd); + + private static native int nGetRenderThreadTid(long nativeProxy); + + private static native long nCreateRootRenderNode(); + + private static native long nCreateProxy(boolean translucent, long rootRenderNode); + + private static native void nDeleteProxy(long nativeProxy); + + private static native boolean nLoadSystemProperties(long nativeProxy); + + private static native void nSetName(long nativeProxy, String name); + + private static native void nSetSurface(long nativeProxy, Surface window); + + private static native boolean nPause(long nativeProxy); + + private static native void nSetStopped(long nativeProxy, boolean stopped); + + private static native void nSetLightGeometry(long nativeProxy, + float lightX, float lightY, float lightZ, float lightRadius); + + private static native void nSetLightAlpha(long nativeProxy, float ambientShadowAlpha, + float spotShadowAlpha); + + private static native void nSetOpaque(long nativeProxy, boolean opaque); + + private static native void nSetWideGamut(long nativeProxy, boolean wideGamut); + + private static native int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size); + + private static native void nDestroy(long nativeProxy, long rootRenderNode); + + private static native void nRegisterAnimatingRenderNode(long rootRenderNode, + long animatingNode); + + private static native void nRegisterVectorDrawableAnimator(long rootRenderNode, long animator); + + private static native void nInvokeFunctor(long functor, boolean waitForCompletion); + + private static native long nCreateTextureLayer(long nativeProxy); + + private static native void nBuildLayer(long nativeProxy, long node); + + private static native boolean nCopyLayerInto(long nativeProxy, long layer, Bitmap bitmap); + + private static native void nPushLayerUpdate(long nativeProxy, long layer); + + private static native void nCancelLayerUpdate(long nativeProxy, long layer); + + private static native void nDetachSurfaceTexture(long nativeProxy, long layer); + + private static native void nDestroyHardwareResources(long nativeProxy); + + private static native void nTrimMemory(int level); + + private static native void nOverrideProperty(String name, String value); + + private static native void nFence(long nativeProxy); + + private static native void nStopDrawing(long nativeProxy); + + private static native void nNotifyFramePending(long nativeProxy); + + private static native void nDumpProfileInfo(long nativeProxy, FileDescriptor fd, + @DumpFlags int dumpFlags); + + private static native void nAddRenderNode(long nativeProxy, long rootRenderNode, + boolean placeFront); + + private static native void nRemoveRenderNode(long nativeProxy, long rootRenderNode); + + private static native void nDrawRenderNode(long nativeProxy, long rootRenderNode); + + private static native void nSetContentDrawBounds(long nativeProxy, int left, + int top, int right, int bottom); + + private static native void nSetFrameCallback(long nativeProxy, FrameDrawingCallback callback); + + private static native void nSetFrameCompleteCallback(long nativeProxy, + FrameCompleteCallback callback); + + private static native long nAddFrameMetricsObserver(long nativeProxy, + FrameMetricsObserver observer); + + private static native void nRemoveFrameMetricsObserver(long nativeProxy, long nativeObserver); + + private static native int nCopySurfaceInto(Surface surface, + int srcLeft, int srcTop, int srcRight, int srcBottom, Bitmap bitmap); + + private static native Bitmap nCreateHardwareBitmap(long renderNode, int width, int height); + + private static native void nSetHighContrastText(boolean enabled); + + // For temporary experimentation b/66945974 + private static native void nHackySetRTAnimationsEnabled(boolean enabled); + + private static native void nSetDebuggingEnabled(boolean enabled); + + private static native void nSetIsolatedProcess(boolean enabled); + + private static native void nSetContextPriority(int priority); + + private static native void nAllocateBuffers(long nativeProxy); + + private static native void nSetForceDark(long nativeProxy, boolean enabled); +} diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 098f10003b8a..009e042f74a7 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -28,6 +28,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Px; import android.annotation.TestApi; +import android.annotation.UnsupportedAppUsage; import android.annotation.WorkerThread; import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; @@ -58,6 +59,7 @@ import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Retention; import java.nio.ByteBuffer; +import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -186,7 +188,7 @@ public final class ImageDecoder implements AutoCloseable { * {@link OnHeaderDecodedListener OnHeaderDecodedListener} and once with an * implementation of {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded} * that calls {@link #setTargetSize} with smaller dimensions. One {@code Source} - * even used simultaneously in multiple threads.</p> + * can even be used simultaneously in multiple threads.</p> */ public static abstract class Source { private Source() {} @@ -282,26 +284,7 @@ public final class ImageDecoder implements AutoCloseable { return createFromStream(is, true, this); } - - final FileDescriptor fd = assetFd.getFileDescriptor(); - final long offset = assetFd.getStartOffset(); - - ImageDecoder decoder = null; - try { - try { - Os.lseek(fd, offset, SEEK_SET); - decoder = nCreate(fd, this); - } catch (ErrnoException e) { - decoder = createFromStream(new FileInputStream(fd), true, this); - } - } finally { - if (decoder == null) { - IoUtils.closeQuietly(assetFd); - } else { - decoder.mAssetFd = assetFd; - } - } - return decoder; + return createFromAssetFileDescriptor(assetFd, this); } } @@ -353,6 +336,30 @@ public final class ImageDecoder implements AutoCloseable { return decoder; } + @NonNull + private static ImageDecoder createFromAssetFileDescriptor(@NonNull AssetFileDescriptor assetFd, + Source source) throws IOException { + final FileDescriptor fd = assetFd.getFileDescriptor(); + final long offset = assetFd.getStartOffset(); + + ImageDecoder decoder = null; + try { + try { + Os.lseek(fd, offset, SEEK_SET); + decoder = nCreate(fd, source); + } catch (ErrnoException e) { + decoder = createFromStream(new FileInputStream(fd), true, source); + } + } finally { + if (decoder == null) { + IoUtils.closeQuietly(assetFd); + } else { + decoder.mAssetFd = assetFd; + } + } + return decoder; + } + /** * For backwards compatibility, this does *not* close the InputStream. * @@ -527,6 +534,29 @@ public final class ImageDecoder implements AutoCloseable { } } + private static class CallableSource extends Source { + CallableSource(@NonNull Callable<AssetFileDescriptor> callable) { + mCallable = callable; + } + + private final Callable<AssetFileDescriptor> mCallable; + + @Override + public ImageDecoder createImageDecoder() throws IOException { + AssetFileDescriptor assetFd = null; + try { + assetFd = mCallable.call(); + } catch (Exception e) { + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new IOException(e); + } + } + return createFromAssetFileDescriptor(assetFd, this); + } + } + /** * Information about an encoded image. */ @@ -970,6 +1000,27 @@ public final class ImageDecoder implements AutoCloseable { } /** + * Create a new {@link Source Source} from a {@link Callable} that returns a + * new {@link AssetFileDescriptor} for each request. This provides control + * over how the {@link AssetFileDescriptor} is created, such as passing + * options into {@link ContentResolver#openTypedAssetFileDescriptor}, or + * enabling use of a {@link android.os.CancellationSignal}. + * <p> + * It's important for the given {@link Callable} to return a new, unique + * {@link AssetFileDescriptor} for each invocation, to support reuse of the + * returned {@link Source Source}. + * + * @return a new Source object, which can be passed to + * {@link #decodeDrawable decodeDrawable} or {@link #decodeBitmap + * decodeBitmap}. + */ + @AnyThread + @NonNull + public static Source createSource(@NonNull Callable<AssetFileDescriptor> callable) { + return new CallableSource(callable); + } + + /** * Return the width and height of a given sample size. * * <p>This takes an input that functions like @@ -1856,6 +1907,7 @@ public final class ImageDecoder implements AutoCloseable { * Private method called by JNI. */ @SuppressWarnings("unused") + @UnsupportedAppUsage private int postProcessAndRelease(@NonNull Canvas canvas) { try { return mPostProcessor.onPostProcess(canvas); diff --git a/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java index 9546a4aec330..c580c46cc888 100644 --- a/graphics/java/android/graphics/ImageFormat.java +++ b/graphics/java/android/graphics/ImageFormat.java @@ -16,8 +16,6 @@ package android.graphics; -import android.annotation.UnsupportedAppUsage; - public class ImageFormat { /* * these constants are chosen to be binary compatible with their previous @@ -92,20 +90,21 @@ public class ImageFormat { * </ul> * </p> * - * <pre> y_size = stride * height </pre> + * <pre> 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> + * 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 1 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 - * - * @hide */ - @UnsupportedAppUsage public static final int Y8 = 0x20203859; /** @@ -787,6 +786,7 @@ public class ImageFormat { case DEPTH_POINT_CLOUD: case PRIVATE: case RAW_DEPTH: + case Y8: return true; } diff --git a/graphics/java/android/graphics/Insets.java b/graphics/java/android/graphics/Insets.java index 156f9903a632..de110c849338 100644 --- a/graphics/java/android/graphics/Insets.java +++ b/graphics/java/android/graphics/Insets.java @@ -16,6 +16,9 @@ package android.graphics; +import android.annotation.NonNull; +import android.annotation.Nullable; + /** * 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 @@ -23,9 +26,8 @@ package android.graphics; * <p> * Insets are immutable so may be treated as values. * - * @hide */ -public class Insets { +public final class Insets { public static final Insets NONE = new Insets(0, 0, 0, 0); public final int left; @@ -52,7 +54,7 @@ public class Insets { * * @return Insets instance with the appropriate values */ - public static Insets of(int left, int top, int right, int bottom) { + public static @NonNull Insets of(int left, int top, int right, int bottom) { if (left == 0 && top == 0 && right == 0 && bottom == 0) { return NONE; } @@ -66,11 +68,20 @@ public class Insets { * * @return an Insets instance with the appropriate values */ - public static Insets of(Rect r) { + public static @NonNull Insets of(@Nullable Rect r) { return (r == null) ? NONE : of(r.left, r.top, r.right, r.bottom); } /** + * Returns a Rect intance with the appropriate values. + * + * @hide + */ + public @NonNull Rect toRect() { + return new Rect(left, top, right, bottom); + } + + /** * Two Insets instances are equal iff they belong to the same class and their fields are * pairwise equal. * diff --git a/graphics/java/android/graphics/Movie.java b/graphics/java/android/graphics/Movie.java index b03eae592211..6f030ffac2df 100644 --- a/graphics/java/android/graphics/Movie.java +++ b/graphics/java/android/graphics/Movie.java @@ -26,6 +26,7 @@ import java.io.InputStream; /** * @deprecated Prefer {@link android.graphics.drawable.AnimatedImageDrawable}. */ +@Deprecated public class Movie { @UnsupportedAppUsage private long mNativeMovie; diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index e229c12d64f1..69ff3bca6528 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -17,10 +17,13 @@ package android.graphics; import android.annotation.ColorInt; +import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.Px; import android.annotation.Size; import android.annotation.UnsupportedAppUsage; -import android.graphics.FontListParser; import android.graphics.fonts.FontVariationAxis; import android.os.LocaleList; import android.text.GraphicsOperations; @@ -35,6 +38,8 @@ import dalvik.annotation.optimization.FastNative; import libcore.util.NativeAllocationRegistry; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -307,38 +312,47 @@ public class Paint { */ public static final int DIRECTION_RTL = 1; + /** @hide */ + @IntDef(prefix = { "CURSOR_" }, value = { + CURSOR_AFTER, CURSOR_AT_OR_AFTER, CURSOR_BEFORE, CURSOR_AT_OR_BEFORE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CursorOption {} + /** - * Option for getTextRunCursor to compute the valid cursor after - * offset or the limit of the context, whichever is less. - * @hide + * Option for getTextRunCursor. + * + * Compute the valid cursor after offset or the limit of the context, whichever is less. */ 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 + * Option for getTextRunCursor. + * + * Compute the valid cursor at or after the offset or the limit of the context, whichever is + * less. */ 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 + * Option for getTextRunCursor. + * + * Compute the valid cursor before offset or the start of the context, whichever is greater. */ 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 + * Option for getTextRunCursor. + * + * Compute the valid cursor at or before offset or the start of the context, whichever is + * greater. */ 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 + * Option for getTextRunCursor. + * + * Return offset if the cursor at offset is valid, or -1 if it isn't. */ public static final int CURSOR_AT = 4; @@ -578,49 +592,6 @@ public class Paint { 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 */ @UnsupportedAppUsage public void setCompatibilityScaling(float factor) { @@ -806,25 +777,39 @@ public class Paint { * 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. + * @see #getUnderlinePosition() + * @see #getUnderlineThickness() + * @see #setUnderlineText(boolean) */ 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 + * Returns the distance from top of the underline to the baseline in pixels. + * + * The result is positive for positions that are below the baseline. + * This method returns where the underline should be drawn independent of if the {@link + * #UNDERLINE_TEXT_FLAG} bit is set. + * + * @return the position of the underline in pixels + * @see #isUnderlineText() + * @see #getUnderlineThickness() + * @see #setUnderlineText(boolean) */ - public float getUnderlinePosition() { + public @Px float getUnderlinePosition() { return nGetUnderlinePosition(mNativePaint); } /** - * @hide + * Returns the thickness of the underline in pixels. + * + * @return the thickness of the underline in pixels + * @see #isUnderlineText() + * @see #getUnderlinePosition() + * @see #setUnderlineText(boolean) */ - public float getUnderlineThickness() { + public @Px float getUnderlineThickness() { return nGetUnderlineThickness(mNativePaint); } @@ -833,6 +818,9 @@ public class Paint { * * @param underlineText true to set the underlineText bit in the paint's * flags, false to clear it. + * @see #isUnderlineText() + * @see #getUnderlinePosition() + * @see #getUnderlineThickness() */ public void setUnderlineText(boolean underlineText) { nSetUnderlineText(mNativePaint, underlineText); @@ -841,26 +829,40 @@ public class Paint { /** * 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. + * @return true if the {@link #STRIKE_THRU_TEXT_FLAG} bit is set in the paint's flags. + * @see #getStrikeThruPosition() + * @see #getStrikeThruThickness() + * @see #setStrikeThruText(boolean) */ 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 + * Distance from top of the strike-through line to the baseline in pixels. + * + * The result is negative for positions that are above the baseline. + * This method returns where the strike-through line should be drawn independent of if the + * {@link #STRIKE_THRU_TEXT_FLAG} bit is set. + * + * @return the position of the strike-through line in pixels + * @see #getStrikeThruThickness() + * @see #setStrikeThruText(boolean) + * @see #isStrikeThruText() */ - public float getStrikeThruPosition() { + public @Px float getStrikeThruPosition() { return nGetStrikeThruPosition(mNativePaint); } /** - * @hide + * Returns the thickness of the strike-through line in pixels. + * + * @return the position of the strike-through line in pixels + * @see #getStrikeThruPosition() + * @see #setStrikeThruText(boolean) + * @see #isStrikeThruText() */ - public float getStrikeThruThickness() { + public @Px float getStrikeThruThickness() { return nGetStrikeThruThickness(mNativePaint); } @@ -869,6 +871,9 @@ public class Paint { * * @param strikeThruText true to set the strikeThruText bit in the paint's * flags, false to clear it. + * @see #getStrikeThruPosition() + * @see #getStrikeThruThickness() + * @see #isStrikeThruText() */ public void setStrikeThruText(boolean strikeThruText) { nSetStrikeThruText(mNativePaint, strikeThruText); @@ -1347,6 +1352,38 @@ public class Paint { } /** + * Returns the blur radius of the shadow layer. + * @see #setShadowLayer(float,float,float,int) + */ + public float getShadowLayerRadius() { + return mShadowLayerRadius; + } + + /** + * Returns the x offset of the shadow layer. + * @see #setShadowLayer(float,float,float,int) + */ + public float getShadowLayerDx() { + return mShadowLayerDx; + } + + /** + * Returns the y offset of the shadow layer. + * @see #setShadowLayer(float,float,float,int) + */ + public float getShadowLayerDy() { + return mShadowLayerDy; + } + + /** + * Returns the color of the shadow layer. + * @see #setShadowLayer(float,float,float,int) + */ + public @ColorInt int getShadowLayerColor() { + return mShadowLayerColor; + } + + /** * 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 @@ -1561,22 +1598,25 @@ public class Paint { } /** - * Return the paint's word-spacing for text. The default value is 0. + * Return the paint's extra word-spacing for text. * - * @return the paint's word-spacing for drawing text. - * @hide + * The default value is 0. + * + * @return the paint's extra word-spacing for drawing text in pixels. + * @see #setWordSpacing(float) */ 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). + * Set the paint's extra word-spacing for text. * - * @param wordSpacing set the paint's word-spacing for drawing text. - * @hide + * Increases the white space width between words with the given amount of pixels. + * The default value is 0. + * + * @param wordSpacing set the paint's extra word-spacing for drawing text in pixels. + * @see #getWordSpacing() */ public void setWordSpacing(float wordSpacing) { nSetWordSpacing(mNativePaint, wordSpacing); @@ -1705,26 +1745,52 @@ public class Paint { } /** - * Get the current value of hyphen edit. + * Get the current value of packed hyphen edit. * - * @return the current hyphen edit value + * You can extract start hyphen edit and end hyphen edit by using + * {@link android.text.Hyphenator#unpackStartHyphenEdit(int)} and + * {@link android.text.Hyphenator#unpackEndHyphenEdit(int)}. * - * @hide + * The default value is 0 which is equivalent to packed value of + * {@link android.text.Hyphenator#START_HYPHEN_EDIT_NO_EDIT} and + * {@link android.text.Hyphenator#END_HYPHEN_EDIT_NO_EDIT}. + * + * @return the current hyphen edit value + * @see #setHyphenEdit(int) */ 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). + * Set a packed hyphen edit on the paint. * - * @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. + * By setting hyphen edit, the measurement and drawing is performed with modifying hyphenation + * at the start of line and end of line. For example, by passing + * {@link android.text.Hyphenator#END_HYPHEN_EDIT_INSERT_HYPHEN} like as follows, HYPHEN(U+2010) + * character is appended at the end of line. * - * @hide + * <pre> + * <code> + * Paint paint = new Paint(); + * paint.setHyphenEdit(Hyphenator.packHyphenEdit( + * Hyphenator.START_HYPHEN_EDIT_NO_EDIT, + * Hyphenator.END_HYPHEN_EDIT_INSERT_HYPHEN)); + * paint.measureText("abc", 0, 3); // Returns the width of "abc‐" + * Canvas.drawText("abc", 0, 3, 0f, 0f, paint); // Draws "abc‐" + * </code> + * </pre> + * + * You can pack start hyphen edit and end hyphen edit by + * {@link android.text.Hyphenator#packHyphenEdit(int,int)} + * + * The default value is 0 which is equivalent to packed value of + * {@link android.text.Hyphenator#START_HYPHEN_EDIT_NO_EDIT} and + * {@link android.text.Hyphenator#END_HYPHEN_EDIT_NO_EDIT}. + * + * @param hyphen a packed hyphen edit value. + * @see #getHyphenEdit() */ - @UnsupportedAppUsage public void setHyphenEdit(int hyphen) { nSetHyphenEdit(mNativePaint, hyphen); } @@ -2262,17 +2328,53 @@ public class Paint { } /** - * Convenience overload that takes a char array instead of a - * String. + * Retrieve the character advances of the text. * - * @see #getTextRunAdvances(String, int, int, int, int, boolean, float[], int) - * @hide - */ - @UnsupportedAppUsage - public float getTextRunAdvances(char[] chars, int index, int count, - int contextIndex, int contextCount, boolean isRtl, float[] advances, - int advancesIndex) { - + * Returns the total advance width for the characters in the run from {@code index} for + * {@code count} of chars, and if {@code 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> + * + * <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> + * + * <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> + * + * <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. + * </p> + * + * @param chars the text to measure. + * @param index the index of the first character to measure + * @param count the number of characters to measure + * @param contextIndex the index of the first character to use for shaping context. + * Context must cover the measureing target. + * @param contextCount the number of character to use for shaping context. + * Context must cover the measureing target. + * @param isRtl whether the run is in RTL direction + * @param advances array to receive the advances, must have room for all advances. + * This 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 in pixels + */ + public float getTextRunAdvances(@NonNull char[] chars, @IntRange(from = 0) int index, + @IntRange(from = 0) int count, @IntRange(from = 0) int contextIndex, + @IntRange(from = 0) int contextCount, boolean isRtl, @Nullable float[] advances, + @IntRange(from = 0) int advancesIndex) { if (chars == null) { throw new IllegalArgumentException("text cannot be null"); } @@ -2309,159 +2411,32 @@ public class Paint { } /** - * Convenience overload that takes a CharSequence instead of a - * String. + * Returns the next cursor position in the run. * - * @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. + * This avoids placing the cursor between surrogates, between characters that form conjuncts, + * between base characters and combining marks, or within a reordering cluster. * - * @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> + * 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. + * <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 isRtl true if the paragraph context is RTL, otherwise false * @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} + * @param cursorOpt how to move the cursor * @return the offset of the next position, or -1 - * @hide */ - @UnsupportedAppUsage - public int getTextRunCursor(char[] text, int contextStart, int contextLength, - int dir, int offset, int cursorOpt) { + public int getTextRunCursor(@NonNull char[] text, @IntRange(from = 0) int contextStart, + @IntRange(from = 0) int contextLength, boolean isRtl, @IntRange(from = 0) int offset, + @CursorOption int cursorOpt) { int contextEnd = contextStart + contextLength; if (((contextStart | contextEnd | offset | (contextEnd - contextStart) | (offset - contextStart) | (contextEnd - offset) @@ -2470,85 +2445,87 @@ public class Paint { throw new IndexOutOfBoundsException(); } - return nGetTextRunCursor(mNativePaint, text, contextStart, contextLength, dir, offset, - cursorOpt); + return nGetTextRunCursor(mNativePaint, text, contextStart, contextLength, + isRtl ? DIRECTION_RTL : DIRECTION_LTR, 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. + * 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 + * <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. + * <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 isRtl true if the paragraph context is RTL, otherwise false * @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} + * @param cursorOpt how to move the cursor * @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) { + public int getTextRunCursor(@NonNull CharSequence text, @IntRange(from = 0) int contextStart, + @IntRange(from = 0) int contextEnd, boolean isRtl, @IntRange(from = 0) int offset, + @CursorOption int cursorOpt) { if (text instanceof String || text instanceof SpannedString || text instanceof SpannableString) { return getTextRunCursor(text.toString(), contextStart, contextEnd, - dir, offset, cursorOpt); + isRtl, offset, cursorOpt); } if (text instanceof GraphicsOperations) { return ((GraphicsOperations) text).getTextRunCursor( - contextStart, contextEnd, dir, offset, cursorOpt, this); + contextStart, contextEnd, isRtl, 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); + int relPos = getTextRunCursor(buf, 0, contextLen, isRtl, 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. + * Returns the next cursor position in the run. * - * <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. + * This avoids placing the cursor between surrogates, between characters that form conjuncts, + * between base characters and combining marks, or within a reordering cluster. * - * <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. + * <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> + * + * <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. + * </p> * * @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 isRtl true if the paragraph context is RTL, otherwise false. * @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} + * @param cursorOpt how to move the cursor * @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) { + public int getTextRunCursor(@NonNull String text, @IntRange(from = 0) int contextStart, + @IntRange(from = 0) int contextEnd, boolean isRtl, @IntRange(from = 0) int offset, + @CursorOption int cursorOpt) { if (((contextStart | contextEnd | offset | (contextEnd - contextStart) | (offset - contextStart) | (contextEnd - offset) | (text.length() - contextEnd) | cursorOpt) < 0) @@ -2556,8 +2533,8 @@ public class Paint { throw new IndexOutOfBoundsException(); } - return nGetTextRunCursor(mNativePaint, text, contextStart, contextEnd, dir, offset, - cursorOpt); + return nGetTextRunCursor(mNativePaint, text, contextStart, contextEnd, + isRtl ? DIRECTION_RTL : DIRECTION_LTR, offset, cursorOpt); } /** @@ -2623,11 +2600,13 @@ public class Paint { * Return in bounds (allocated by the caller) the smallest rectangle that * encloses all of the characters, with an implied origin at (0,0). * + * Note that styles are ignored even if you pass {@link android.text.Spanned} instance. + * Use {@link android.text.StaticLayout} for measuring bounds of {@link android.text.Spanned}. + * * @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) { diff --git a/graphics/java/android/graphics/Path.java b/graphics/java/android/graphics/Path.java index ec48d2c91872..405ab0bbcb34 100644 --- a/graphics/java/android/graphics/Path.java +++ b/graphics/java/android/graphics/Path.java @@ -69,7 +69,7 @@ public class Path { * * @param src The path to copy from when initializing the new path */ - public Path(Path src) { + public Path(@Nullable Path src) { long valNative = 0; if (src != null) { valNative = src.mNativePath; @@ -171,7 +171,7 @@ public class Path { * @see Op * @see #op(Path, Path, android.graphics.Path.Op) */ - public boolean op(Path path, Op op) { + public boolean op(@NonNull Path path, @NonNull Op op) { return op(this, path, op); } @@ -189,7 +189,7 @@ public class Path { * @see Op * @see #op(Path, android.graphics.Path.Op) */ - public boolean op(Path path1, Path path2, Op op) { + public boolean op(@NonNull Path path1, @NonNull Path path2, @NonNull Op op) { if (nOp(path1.mNativePath, path2.mNativePath, op.ordinal(), this.mNativePath)) { isSimplePath = false; rects = null; @@ -258,6 +258,7 @@ public class Path { * * @return the path's fill type */ + @NonNull public FillType getFillType() { return sFillTypeArray[nGetFillType(mNativePath)]; } @@ -267,7 +268,7 @@ public class Path { * * @param ft The new fill type for this path */ - public void setFillType(FillType ft) { + public void setFillType(@NonNull FillType ft) { nSetFillType(mNativePath, ft.nativeInt); } @@ -321,7 +322,7 @@ public class Path { * @param exact This parameter is no longer used. */ @SuppressWarnings({"UnusedDeclaration"}) - public void computeBounds(RectF bounds, boolean exact) { + public void computeBounds(@NonNull RectF bounds, boolean exact) { nComputeBounds(mNativePath, bounds); } @@ -464,7 +465,7 @@ public class Path { * mod 360. * @param forceMoveTo If true, always begin a new contour with the arc */ - public void arcTo(RectF oval, float startAngle, float sweepAngle, + public void arcTo(@NonNull RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) { arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo); } @@ -480,7 +481,7 @@ public class Path { * @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) { + public void arcTo(@NonNull RectF oval, float startAngle, float sweepAngle) { arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false); } @@ -545,7 +546,7 @@ public class 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) { + public void addRect(@NonNull RectF rect, @NonNull Direction dir) { addRect(rect.left, rect.top, rect.right, rect.bottom, dir); } @@ -558,7 +559,7 @@ public class 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) { + public void addRect(float left, float top, float right, float bottom, @NonNull Direction dir) { detectSimplePath(left, top, right, bottom, dir); nAddRect(mNativePath, left, top, right, bottom, dir.nativeInt); } @@ -569,7 +570,7 @@ public class 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) { + public void addOval(@NonNull RectF oval, @NonNull Direction dir) { addOval(oval.left, oval.top, oval.right, oval.bottom, dir); } @@ -578,7 +579,7 @@ public class Path { * * @param dir The direction to wind the oval's contour */ - public void addOval(float left, float top, float right, float bottom, Direction dir) { + public void addOval(float left, float top, float right, float bottom, @NonNull Direction dir) { isSimplePath = false; nAddOval(mNativePath, left, top, right, bottom, dir.nativeInt); } @@ -591,7 +592,7 @@ public class 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) { + public void addCircle(float x, float y, float radius, @NonNull Direction dir) { isSimplePath = false; nAddCircle(mNativePath, x, y, radius, dir.nativeInt); } @@ -603,7 +604,7 @@ public class Path { * @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) { + public void addArc(@NonNull RectF oval, float startAngle, float sweepAngle) { addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle); } @@ -627,7 +628,7 @@ public class Path { * @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) { + public void addRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Direction dir) { addRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, dir); } @@ -639,7 +640,7 @@ public class Path { * @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) { + @NonNull Direction dir) { isSimplePath = false; nAddRoundRect(mNativePath, left, top, right, bottom, rx, ry, dir.nativeInt); } @@ -653,7 +654,7 @@ public class 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) { + public void addRoundRect(@NonNull RectF rect, @NonNull float[] radii, @NonNull Direction dir) { if (rect == null) { throw new NullPointerException("need rect parameter"); } @@ -668,8 +669,8 @@ public class 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(float left, float top, float right, float bottom, float[] radii, - Direction dir) { + public void addRoundRect(float left, float top, float right, float bottom, + @NonNull float[] radii, @NonNull Direction dir) { if (radii.length < 8) { throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values"); } @@ -683,7 +684,7 @@ public class Path { * @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) { + public void addPath(@NonNull Path src, float dx, float dy) { isSimplePath = false; nAddPath(mNativePath, src.mNativePath, dx, dy); } @@ -693,7 +694,7 @@ public class Path { * * @param src The path that is appended to the current path */ - public void addPath(Path src) { + public void addPath(@NonNull Path src) { isSimplePath = false; nAddPath(mNativePath, src.mNativePath); } @@ -703,7 +704,7 @@ public class Path { * * @param src The path to add as a new contour */ - public void addPath(Path src, Matrix matrix) { + public void addPath(@NonNull Path src, @NonNull Matrix matrix) { if (!src.isSimplePath) isSimplePath = false; nAddPath(mNativePath, src.mNativePath, matrix.native_instance); } @@ -763,7 +764,7 @@ public class 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) { + public void transform(@NonNull Matrix matrix, @Nullable Path dst) { long dstNative = 0; if (dst != null) { dst.isSimplePath = false; @@ -777,7 +778,7 @@ public class Path { * * @param matrix The matrix to apply to the path */ - public void transform(Matrix matrix) { + public void transform(@NonNull Matrix matrix) { isSimplePath = false; nTransform(mNativePath, matrix.native_instance); } diff --git a/graphics/java/android/graphics/Picture.java b/graphics/java/android/graphics/Picture.java index b36d752ba5d1..f6d801b3ba43 100644 --- a/graphics/java/android/graphics/Picture.java +++ b/graphics/java/android/graphics/Picture.java @@ -17,6 +17,7 @@ package android.graphics; import android.annotation.UnsupportedAppUsage; + import java.io.InputStream; import java.io.OutputStream; @@ -165,6 +166,7 @@ public class Picture { * properly and are highly discouraged. * * @see #writeToStream(java.io.OutputStream) + * @removed * @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. @@ -181,6 +183,7 @@ public class Picture { * there is no guarantee that the Picture can be successfully reconstructed. * * @see #createFromStream(java.io.InputStream) + * @removed * @deprecated The recommended alternative is to draw the picture into a * Bitmap from which you can persist it as raw or compressed pixels. */ @@ -214,7 +217,7 @@ public class Picture { public PictureCanvas(Picture pict, long nativeCanvas) { super(nativeCanvas); mPicture = pict; - // Disable bitmap density scaling. This matches DisplayListCanvas. + // Disable bitmap density scaling. This matches RecordingCanvas. mDensity = 0; } diff --git a/graphics/java/android/graphics/Point.java b/graphics/java/android/graphics/Point.java index c6b6c668f03a..ea9350174f97 100644 --- a/graphics/java/android/graphics/Point.java +++ b/graphics/java/android/graphics/Point.java @@ -16,8 +16,10 @@ package android.graphics; +import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; +import android.util.Size; import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; @@ -37,7 +39,7 @@ public class Point implements Parcelable { this.y = y; } - public Point(Point src) { + public Point(@NonNull Point src) { this.x = src.x; this.y = src.y; } @@ -99,7 +101,7 @@ public class Point implements Parcelable { } /** @hide */ - public void printShortString(PrintWriter pw) { + public void printShortString(@NonNull PrintWriter pw) { pw.print("["); pw.print(x); pw.print(","); pw.print(y); pw.print("]"); } @@ -130,7 +132,7 @@ public class Point implements Parcelable { * @param fieldId Field Id of the Rect as defined in the parent message * @hide */ - public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) { + public void writeToProto(@NonNull ProtoOutputStream protoOutputStream, long fieldId) { final long token = protoOutputStream.start(fieldId); protoOutputStream.write(PointProto.X, x); protoOutputStream.write(PointProto.Y, y); @@ -141,6 +143,7 @@ public class Point implements Parcelable { /** * Return a new point from the data in the specified parcel. */ + @Override public Point createFromParcel(Parcel in) { Point r = new Point(); r.readFromParcel(in); @@ -150,6 +153,7 @@ public class Point implements Parcelable { /** * Return an array of rectangles of the specified size. */ + @Override public Point[] newArray(int size) { return new Point[size]; } @@ -161,8 +165,18 @@ public class Point implements Parcelable { * * @param in The parcel to read the point's coordinates from */ - public void readFromParcel(Parcel in) { + public void readFromParcel(@NonNull Parcel in) { x = in.readInt(); y = in.readInt(); } + + /** {@hide} */ + public static @NonNull Point convert(@NonNull Size size) { + return new Point(size.getWidth(), size.getHeight()); + } + + /** {@hide} */ + public static @NonNull Size convert(@NonNull Point point) { + return new Size(point.x, point.y); + } } diff --git a/graphics/java/android/graphics/PointF.java b/graphics/java/android/graphics/PointF.java index 8e4288e35c42..a3b41944f7ab 100644 --- a/graphics/java/android/graphics/PointF.java +++ b/graphics/java/android/graphics/PointF.java @@ -16,10 +16,10 @@ package android.graphics; +import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; - /** * PointF holds two float coordinates */ @@ -34,7 +34,7 @@ public class PointF implements Parcelable { this.y = y; } - public PointF(Point p) { + public PointF(@NonNull Point p) { this.x = p.x; this.y = p.y; } @@ -50,7 +50,7 @@ public class PointF implements Parcelable { /** * Set the point's x and y coordinates to the coordinates of p */ - public final void set(PointF p) { + public final void set(@NonNull PointF p) { this.x = p.x; this.y = p.y; } @@ -134,6 +134,7 @@ public class PointF implements Parcelable { /** * Return a new point from the data in the specified parcel. */ + @Override public PointF createFromParcel(Parcel in) { PointF r = new PointF(); r.readFromParcel(in); @@ -143,6 +144,7 @@ public class PointF implements Parcelable { /** * Return an array of rectangles of the specified size. */ + @Override public PointF[] newArray(int size) { return new PointF[size]; } @@ -154,7 +156,7 @@ public class PointF implements Parcelable { * * @param in The parcel to read the point's coordinates from */ - public void readFromParcel(Parcel in) { + public void readFromParcel(@NonNull Parcel in) { x = in.readFloat(); y = in.readFloat(); } diff --git a/graphics/java/android/graphics/PorterDuffColorFilter.java b/graphics/java/android/graphics/PorterDuffColorFilter.java index 3c90b72eaed0..6665220c293c 100644 --- a/graphics/java/android/graphics/PorterDuffColorFilter.java +++ b/graphics/java/android/graphics/PorterDuffColorFilter.java @@ -36,8 +36,6 @@ public class PorterDuffColorFilter extends ColorFilter { * @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; @@ -49,7 +47,6 @@ public class PorterDuffColorFilter extends ColorFilter { * is applied. * * @see Color - * @see #setColor(int) * * @hide */ @@ -60,30 +57,10 @@ public class PorterDuffColorFilter extends ColorFilter { } /** - * 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 */ @@ -92,24 +69,6 @@ public class PorterDuffColorFilter extends ColorFilter { 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); diff --git a/graphics/java/android/graphics/RecordingCanvas.java b/graphics/java/android/graphics/RecordingCanvas.java new file mode 100644 index 000000000000..fd5d62406da7 --- /dev/null +++ b/graphics/java/android/graphics/RecordingCanvas.java @@ -0,0 +1,304 @@ +/* + * 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.NonNull; +import android.annotation.Nullable; +import android.util.Pools.SynchronizedPool; +import android.view.TextureLayer; + +import dalvik.annotation.optimization.CriticalNative; +import dalvik.annotation.optimization.FastNative; + +/** + * A Canvas implementation that records view system drawing operations for deferred rendering. + * This is used in combination with RenderNode. This class keeps a list of all the Paint and + * Bitmap objects that it draws, preventing the backing memory of Bitmaps from being released while + * the RecordingCanvas is still holding a native reference to the memory. + * + * This is obtained by calling {@link RenderNode#startRecording()} and is valid until the matching + * {@link RenderNode#endRecording()} is called. It must not be retained beyond that as it is + * internally reused. + */ +public final class RecordingCanvas extends BaseRecordingCanvas { + // The recording canvas pool should be large enough to handle a deeply nested + // view hierarchy because display lists are generated recursively. + private static final int POOL_LIMIT = 25; + + /** @hide */ + public static final int MAX_BITMAP_SIZE = 100 * 1024 * 1024; // 100 MB + + private static final SynchronizedPool<RecordingCanvas> sPool = + new SynchronizedPool<>(POOL_LIMIT); + + /** + * TODO: Temporarily exposed for RenderNodeAnimator(Set) + * @hide */ + public RenderNode mNode; + private int mWidth; + private int mHeight; + + /** @hide */ + static RecordingCanvas obtain(@NonNull RenderNode node, int width, int height) { + if (node == null) throw new IllegalArgumentException("node cannot be null"); + RecordingCanvas canvas = sPool.acquire(); + if (canvas == null) { + canvas = new RecordingCanvas(node, width, height); + } else { + nResetDisplayListCanvas(canvas.mNativeCanvasWrapper, node.mNativeRenderNode, + width, height); + } + canvas.mNode = node; + canvas.mWidth = width; + canvas.mHeight = height; + return canvas; + } + + /** @hide */ + void recycle() { + mNode = null; + sPool.release(this); + } + + /** @hide */ + long finishRecording() { + return nFinishRecording(mNativeCanvasWrapper); + } + + /** @hide */ + @Override + public boolean isRecordingFor(Object o) { + return o == mNode; + } + + /////////////////////////////////////////////////////////////////////////// + // Constructors + /////////////////////////////////////////////////////////////////////////// + + private RecordingCanvas(@NonNull RenderNode node, int width, int height) { + super(nCreateDisplayListCanvas(node.mNativeRenderNode, width, height)); + mDensity = 0; // disable bitmap density scaling + } + + /////////////////////////////////////////////////////////////////////////// + // Canvas management + /////////////////////////////////////////////////////////////////////////// + + + @Override + public void setDensity(int density) { + // drop silently, since RecordingCanvas doesn't perform density scaling + } + + @Override + public boolean isHardwareAccelerated() { + return true; + } + + @Override + public void setBitmap(Bitmap bitmap) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOpaque() { + return false; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public int getMaximumBitmapWidth() { + return nGetMaximumTextureWidth(); + } + + @Override + public int getMaximumBitmapHeight() { + return nGetMaximumTextureHeight(); + } + + /////////////////////////////////////////////////////////////////////////// + // Setup + /////////////////////////////////////////////////////////////////////////// + + @Override + public void enableZ() { + nInsertReorderBarrier(mNativeCanvasWrapper, true); + } + + @Override + public void disableZ() { + nInsertReorderBarrier(mNativeCanvasWrapper, false); + } + + /////////////////////////////////////////////////////////////////////////// + // Functor + /////////////////////////////////////////////////////////////////////////// + + /** + * Records the functor specified with the drawGLFunction function pointer. This is + * functionality used by webview for calling into their renderer from our display lists. + * + * @param drawGLFunction A native function pointer + * + * @hide + */ + public void callDrawGLFunction2(long drawGLFunction) { + nCallDrawGLFunction(mNativeCanvasWrapper, drawGLFunction, null); + } + + /** + * Records the functor specified with the drawGLFunction function pointer. This is + * functionality used by webview for calling into their renderer from our display lists. + * + * @param drawGLFunctor A native function pointer + * @param releasedCallback Called when the display list is destroyed, and thus + * the functor is no longer referenced by this canvas's display list. + * + * NOTE: The callback does *not* necessarily mean that there are no longer + * any references to the functor, just that the reference from this specific + * canvas's display list has been released. + * + * @hide + */ + public void drawGLFunctor2(long drawGLFunctor, @Nullable Runnable releasedCallback) { + nCallDrawGLFunction(mNativeCanvasWrapper, drawGLFunctor, releasedCallback); + } + + /////////////////////////////////////////////////////////////////////////// + // Display list + /////////////////////////////////////////////////////////////////////////// + + /** + * Draws the specified display list onto this canvas. + * + * @param renderNode The RenderNode to draw. + */ + @Override + public void drawRenderNode(@NonNull RenderNode renderNode) { + nDrawRenderNode(mNativeCanvasWrapper, renderNode.mNativeRenderNode); + } + + /////////////////////////////////////////////////////////////////////////// + // Hardware layer + /////////////////////////////////////////////////////////////////////////// + + /** + * Draws the specified layer onto this canvas. + * + * @param layer The layer to composite on this canvas + * @hide + */ + public void drawTextureLayer(TextureLayer layer) { + nDrawTextureLayer(mNativeCanvasWrapper, layer.getLayerHandle()); + } + + /////////////////////////////////////////////////////////////////////////// + // Drawing + /////////////////////////////////////////////////////////////////////////// + + /** + * Draws a circle + * + * @param cx + * @param cy + * @param radius + * @param paint + * + * @hide + */ + public void drawCircle(CanvasProperty<Float> cx, CanvasProperty<Float> cy, + CanvasProperty<Float> radius, CanvasProperty<Paint> paint) { + nDrawCircle(mNativeCanvasWrapper, cx.getNativeContainer(), cy.getNativeContainer(), + radius.getNativeContainer(), paint.getNativeContainer()); + } + + /** + * Draws a round rect + * + * @param left + * @param top + * @param right + * @param bottom + * @param rx + * @param ry + * @param paint + * + * @hide + */ + public void drawRoundRect(CanvasProperty<Float> left, CanvasProperty<Float> top, + CanvasProperty<Float> right, CanvasProperty<Float> bottom, CanvasProperty<Float> rx, + CanvasProperty<Float> ry, CanvasProperty<Paint> paint) { + nDrawRoundRect(mNativeCanvasWrapper, left.getNativeContainer(), top.getNativeContainer(), + right.getNativeContainer(), bottom.getNativeContainer(), + rx.getNativeContainer(), ry.getNativeContainer(), + paint.getNativeContainer()); + } + + /** @hide */ + @Override + protected void throwIfCannotDraw(Bitmap bitmap) { + super.throwIfCannotDraw(bitmap); + int bitmapSize = bitmap.getByteCount(); + if (bitmapSize > MAX_BITMAP_SIZE) { + throw new RuntimeException( + "Canvas: trying to draw too large(" + bitmapSize + "bytes) bitmap."); + } + } + + + // ------------------ Fast JNI ------------------------ + + @FastNative + private static native void nCallDrawGLFunction(long renderer, + long drawGLFunction, Runnable releasedCallback); + + + // ------------------ Critical JNI ------------------------ + + @CriticalNative + private static native long nCreateDisplayListCanvas(long node, int width, int height); + @CriticalNative + private static native void nResetDisplayListCanvas(long canvas, long node, + int width, int height); + @CriticalNative + private static native int nGetMaximumTextureWidth(); + @CriticalNative + private static native int nGetMaximumTextureHeight(); + @CriticalNative + private static native void nInsertReorderBarrier(long renderer, boolean enableReorder); + @CriticalNative + private static native long nFinishRecording(long renderer); + @CriticalNative + private static native void nDrawRenderNode(long renderer, long renderNode); + @CriticalNative + private static native void nDrawTextureLayer(long renderer, long layer); + @CriticalNative + private static native void nDrawCircle(long renderer, long propCx, + long propCy, long propRadius, long propPaint); + @CriticalNative + private static native void nDrawRoundRect(long renderer, long propLeft, long propTop, + long propRight, long propBottom, long propRx, long propRy, long propPaint); +} diff --git a/graphics/java/android/graphics/Rect.java b/graphics/java/android/graphics/Rect.java index 9cada3c5b729..c4dc0adb3be0 100644 --- a/graphics/java/android/graphics/Rect.java +++ b/graphics/java/android/graphics/Rect.java @@ -17,13 +17,17 @@ package android.graphics; import android.annotation.CheckResult; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.UnsupportedAppUsage; import android.os.Parcel; import android.os.Parcelable; - -import android.annotation.UnsupportedAppUsage; import android.text.TextUtils; +import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; +import android.util.proto.WireTypeMismatchException; + +import java.io.IOException; import java.io.PrintWriter; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -90,7 +94,7 @@ public final class Rect implements Parcelable { * @param r The rectangle whose coordinates are copied into the new * rectangle. */ - public Rect(Rect r) { + public Rect(@Nullable Rect r) { if (r == null) { left = top = right = bottom = 0; } else { @@ -141,6 +145,7 @@ public final class Rect implements Parcelable { /** * Return a string representation of the rectangle in a compact form. */ + @NonNull public String toShortString() { return toShortString(new StringBuilder(32)); } @@ -149,7 +154,8 @@ public final class Rect implements Parcelable { * Return a string representation of the rectangle in a compact form. * @hide */ - public String toShortString(StringBuilder sb) { + @NonNull + public String toShortString(@NonNull StringBuilder sb) { sb.setLength(0); sb.append('['); sb.append(left); sb.append(','); sb.append(top); sb.append("]["); sb.append(right); @@ -165,6 +171,7 @@ public final class Rect implements Parcelable { * * @return Returns a new String of the form "left top right bottom" */ + @NonNull public String flattenToString() { StringBuilder sb = new StringBuilder(32); // WARNING: Do not change the format of this string, it must be @@ -183,7 +190,8 @@ public final class Rect implements Parcelable { * 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) { + @Nullable + public static Rect unflattenFromString(@Nullable String str) { if (TextUtils.isEmpty(str)) { return null; } @@ -203,7 +211,7 @@ public final class Rect implements Parcelable { * @hide */ @UnsupportedAppUsage - public void printShortString(PrintWriter pw) { + public void printShortString(@NonNull PrintWriter pw) { pw.print('['); pw.print(left); pw.print(','); pw.print(top); pw.print("]["); pw.print(right); pw.print(','); pw.print(bottom); pw.print(']'); @@ -217,7 +225,7 @@ public final class Rect implements Parcelable { * @param fieldId Field Id of the Rect as defined in the parent message * @hide */ - public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) { + public void writeToProto(@NonNull ProtoOutputStream protoOutputStream, long fieldId) { final long token = protoOutputStream.start(fieldId); protoOutputStream.write(RectProto.LEFT, left); protoOutputStream.write(RectProto.TOP, top); @@ -227,6 +235,40 @@ public final class Rect implements Parcelable { } /** + * Read from a protocol buffer input stream. + * Protocol buffer message definition at {@link android.graphics.RectProto} + * + * @param proto Stream to read the Rect object from. + * @param fieldId Field Id of the Rect as defined in the parent message + * @hide + */ + public void readFromProto(@NonNull ProtoInputStream proto, long fieldId) throws IOException, + WireTypeMismatchException { + final long token = proto.start(fieldId); + try { + while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (proto.getFieldNumber()) { + case (int) RectProto.LEFT: + left = proto.readInt(RectProto.LEFT); + break; + case (int) RectProto.TOP: + top = proto.readInt(RectProto.TOP); + break; + case (int) RectProto.RIGHT: + right = proto.readInt(RectProto.RIGHT); + break; + case (int) RectProto.BOTTOM: + bottom = proto.readInt(RectProto.BOTTOM); + break; + } + } + } finally { + // Let caller handle any exceptions + proto.end(token); + } + } + + /** * Returns true if the rectangle is empty (left >= right or top >= bottom) */ public final boolean isEmpty() { @@ -311,7 +353,7 @@ public final class Rect implements Parcelable { * @param src The rectangle whose coordinates are copied into this * rectangle. */ - public void set(Rect src) { + public void set(@NonNull Rect src) { this.left = src.left; this.top = src.top; this.right = src.right; @@ -368,7 +410,7 @@ public final class Rect implements Parcelable { * @hide * @param insets The rectangle specifying the insets on all side. */ - public void inset(Rect insets) { + public void inset(@NonNull Rect insets) { left += insets.left; top += insets.top; right -= insets.right; @@ -434,7 +476,7 @@ public final class Rect implements Parcelable { * @return true iff the specified rectangle r is inside or equal to this * rectangle */ - public boolean contains(Rect r) { + public boolean contains(@NonNull Rect r) { // check for empty first return this.left < this.right && this.top < this.bottom // now check for containment @@ -483,7 +525,7 @@ public final class Rect implements Parcelable { * return false and do not change this rectangle. */ @CheckResult - public boolean intersect(Rect r) { + public boolean intersect(@NonNull Rect r) { return intersect(r.left, r.top, r.right, r.bottom); } @@ -493,7 +535,7 @@ public final class Rect implements Parcelable { * @see #inset(int, int, int, int) but without checking if the rects overlap. * @hide */ - public void intersectUnchecked(Rect other) { + public void intersectUnchecked(@NonNull Rect other) { left = Math.max(left, other.left); top = Math.max(top, other.top); right = Math.min(right, other.right); @@ -513,7 +555,7 @@ public final class Rect implements Parcelable { * false and do not change this rectangle. */ @CheckResult - public boolean setIntersect(Rect a, Rect b) { + public boolean setIntersect(@NonNull Rect a, @NonNull 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); @@ -552,7 +594,7 @@ public final class Rect implements Parcelable { * @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) { + public static boolean intersects(@NonNull Rect a, @NonNull Rect b) { return a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom; } @@ -589,7 +631,7 @@ public final class Rect implements Parcelable { * * @param r The rectangle being unioned with this rectangle */ - public void union(Rect r) { + public void union(@NonNull Rect r) { union(r.left, r.top, r.right, r.bottom); } @@ -636,6 +678,7 @@ public final class Rect implements Parcelable { /** * Parcelable interface methods */ + @Override public int describeContents() { return 0; } @@ -645,6 +688,7 @@ public final class Rect implements Parcelable { * a parcel, use readFromParcel() * @param out The parcel to write the rectangle's coordinates into */ + @Override public void writeToParcel(Parcel out, int flags) { out.writeInt(left); out.writeInt(top); @@ -656,6 +700,7 @@ public final class Rect implements Parcelable { /** * Return a new rectangle from the data in the specified parcel. */ + @Override public Rect createFromParcel(Parcel in) { Rect r = new Rect(); r.readFromParcel(in); @@ -665,6 +710,7 @@ public final class Rect implements Parcelable { /** * Return an array of rectangles of the specified size. */ + @Override public Rect[] newArray(int size) { return new Rect[size]; } @@ -676,7 +722,7 @@ public final class Rect implements Parcelable { * * @param in The parcel to read the rectangle's coordinates from */ - public void readFromParcel(Parcel in) { + public void readFromParcel(@NonNull Parcel in) { left = in.readInt(); top = in.readInt(); right = in.readInt(); diff --git a/graphics/java/android/graphics/RectF.java b/graphics/java/android/graphics/RectF.java index b49054550956..d6447ac237ab 100644 --- a/graphics/java/android/graphics/RectF.java +++ b/graphics/java/android/graphics/RectF.java @@ -16,12 +16,15 @@ package android.graphics; -import java.io.PrintWriter; - +import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; + import com.android.internal.util.FastMath; +import java.io.PrintWriter; + /** * RectF holds four float coordinates for a rectangle. The rectangle is * represented by the coordinates of its 4 edges (left, top, right bottom). @@ -64,7 +67,7 @@ public class RectF implements Parcelable { * @param r The rectangle whose coordinates are copied into the new * rectangle. */ - public RectF(RectF r) { + public RectF(@Nullable RectF r) { if (r == null) { left = top = right = bottom = 0.0f; } else { @@ -75,7 +78,7 @@ public class RectF implements Parcelable { } } - public RectF(Rect r) { + public RectF(@Nullable Rect r) { if (r == null) { left = top = right = bottom = 0.0f; } else { @@ -104,6 +107,7 @@ public class RectF implements Parcelable { return result; } + @Override public String toString() { return "RectF(" + left + ", " + top + ", " + right + ", " + bottom + ")"; @@ -112,6 +116,7 @@ public class RectF implements Parcelable { /** * Return a string representation of the rectangle in a compact form. */ + @NonNull public String toShortString() { return toShortString(new StringBuilder(32)); } @@ -120,7 +125,8 @@ public class RectF implements Parcelable { * Return a string representation of the rectangle in a compact form. * @hide */ - public String toShortString(StringBuilder sb) { + @NonNull + public String toShortString(@NonNull StringBuilder sb) { sb.setLength(0); sb.append('['); sb.append(left); sb.append(','); sb.append(top); sb.append("]["); sb.append(right); @@ -132,7 +138,7 @@ public class RectF implements Parcelable { * Print short representation to given writer. * @hide */ - public void printShortString(PrintWriter pw) { + public void printShortString(@NonNull PrintWriter pw) { pw.print('['); pw.print(left); pw.print(','); pw.print(top); pw.print("]["); pw.print(right); pw.print(','); pw.print(bottom); pw.print(']'); @@ -207,7 +213,7 @@ public class RectF implements Parcelable { * @param src The rectangle whose coordinates are copied into this * rectangle. */ - public void set(RectF src) { + public void set(@NonNull RectF src) { this.left = src.left; this.top = src.top; this.right = src.right; @@ -220,7 +226,7 @@ public class RectF implements Parcelable { * @param src The rectangle whose coordinates are copied into this * rectangle. */ - public void set(Rect src) { + public void set(@NonNull Rect src) { this.left = src.left; this.top = src.top; this.right = src.right; @@ -315,7 +321,7 @@ public class RectF implements Parcelable { * @return true iff the specified rectangle r is inside or equal to this * rectangle */ - public boolean contains(RectF r) { + public boolean contains(@NonNull RectF r) { // check for empty first return this.left < this.right && this.top < this.bottom // now check for containment @@ -372,7 +378,7 @@ public class RectF implements Parcelable { * (and this rectangle is then set to that intersection) else * return false and do not change this rectangle. */ - public boolean intersect(RectF r) { + public boolean intersect(@NonNull RectF r) { return intersect(r.left, r.top, r.right, r.bottom); } @@ -388,7 +394,7 @@ public class RectF implements Parcelable { * this rectangle to that intersection. If they do not, return * false and do not change this rectangle. */ - public boolean setIntersect(RectF a, RectF b) { + public boolean setIntersect(@NonNull RectF a, @NonNull 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); @@ -430,7 +436,7 @@ public class RectF implements Parcelable { * @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) { + public static boolean intersects(@NonNull RectF a, @NonNull RectF b) { return a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom; } @@ -439,7 +445,7 @@ public class RectF implements Parcelable { * Set the dst integer Rect by rounding this rectangle's coordinates * to their nearest integer values. */ - public void round(Rect dst) { + public void round(@NonNull Rect dst) { dst.set(FastMath.round(left), FastMath.round(top), FastMath.round(right), FastMath.round(bottom)); } @@ -448,7 +454,7 @@ public class RectF implements Parcelable { * 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) { + public void roundOut(@NonNull Rect dst) { dst.set((int) Math.floor(left), (int) Math.floor(top), (int) Math.ceil(right), (int) Math.ceil(bottom)); } @@ -490,7 +496,7 @@ public class RectF implements Parcelable { * * @param r The rectangle being unioned with this rectangle */ - public void union(RectF r) { + public void union(@NonNull RectF r) { union(r.left, r.top, r.right, r.bottom); } @@ -537,6 +543,7 @@ public class RectF implements Parcelable { /** * Parcelable interface methods */ + @Override public int describeContents() { return 0; } @@ -546,6 +553,7 @@ public class RectF implements Parcelable { * a parcel, use readFromParcel() * @param out The parcel to write the rectangle's coordinates into */ + @Override public void writeToParcel(Parcel out, int flags) { out.writeFloat(left); out.writeFloat(top); @@ -557,6 +565,7 @@ public class RectF implements Parcelable { /** * Return a new rectangle from the data in the specified parcel. */ + @Override public RectF createFromParcel(Parcel in) { RectF r = new RectF(); r.readFromParcel(in); @@ -566,6 +575,7 @@ public class RectF implements Parcelable { /** * Return an array of rectangles of the specified size. */ + @Override public RectF[] newArray(int size) { return new RectF[size]; } @@ -577,7 +587,7 @@ public class RectF implements Parcelable { * * @param in The parcel to read the rectangle's coordinates from */ - public void readFromParcel(Parcel in) { + public void readFromParcel(@NonNull Parcel in) { left = in.readFloat(); top = in.readFloat(); right = in.readFloat(); diff --git a/graphics/java/android/graphics/Region.java b/graphics/java/android/graphics/Region.java index db6ebd5f22c3..29d936778d44 100644 --- a/graphics/java/android/graphics/Region.java +++ b/graphics/java/android/graphics/Region.java @@ -16,6 +16,7 @@ package android.graphics; +import android.annotation.NonNull; import android.annotation.UnsupportedAppUsage; import android.os.Parcel; import android.os.Parcelable; @@ -62,14 +63,14 @@ public class Region implements Parcelable { /** Return a copy of the specified region */ - public Region(Region region) { + public Region(@NonNull Region region) { this(nativeConstructor()); nativeSetRegion(mNativeRegion, region.mNativeRegion); } /** Return a region set to the specified rectangle */ - public Region(Rect r) { + public Region(@NonNull Rect r) { mNativeRegion = nativeConstructor(); nativeSetRect(mNativeRegion, r.left, r.top, r.right, r.bottom); } @@ -89,14 +90,14 @@ public class Region implements Parcelable { /** Set the region to the specified region. */ - public boolean set(Region region) { + public boolean set(@NonNull Region region) { nativeSetRegion(mNativeRegion, region.mNativeRegion); return true; } /** Set the region to the specified rectangle */ - public boolean set(Rect r) { + public boolean set(@NonNull Rect r) { return nativeSetRect(mNativeRegion, r.left, r.top, r.right, r.bottom); } @@ -112,7 +113,7 @@ public class Region implements Parcelable { * that is identical to the pixels that would be drawn by the path * (with no antialiasing). */ - public boolean setPath(Path path, Region clip) { + public boolean setPath(@NonNull Path path, @NonNull Region clip) { return nativeSetPath(mNativeRegion, path.readOnlyNI(), clip.mNativeRegion); } @@ -135,6 +136,7 @@ public class Region implements Parcelable { * 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] */ + @NonNull public Rect getBounds() { Rect r = new Rect(); nativeGetBounds(mNativeRegion, r); @@ -145,7 +147,7 @@ public class Region implements Parcelable { * 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) { + public boolean getBounds(@NonNull Rect r) { if (r == null) { throw new NullPointerException(); } @@ -156,6 +158,7 @@ public class Region implements Parcelable { * Return the boundary of the region as a new Path. If the region is empty, * the path will also be empty. */ + @NonNull public Path getBoundaryPath() { Path path = new Path(); nativeGetBoundaryPath(mNativeRegion, path.mutateNI()); @@ -166,7 +169,7 @@ public class Region implements Parcelable { * 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) { + public boolean getBoundaryPath(@NonNull Path path) { return nativeGetBoundaryPath(mNativeRegion, path.mutateNI()); } @@ -181,7 +184,7 @@ public class Region implements Parcelable { * 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) { + public boolean quickContains(@NonNull Rect r) { return quickContains(r.left, r.top, r.right, r.bottom); } @@ -199,7 +202,7 @@ public class Region implements Parcelable { * 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) { + public boolean quickReject(@NonNull Rect r) { return quickReject(r.left, r.top, r.right, r.bottom); } @@ -251,7 +254,7 @@ public class Region implements Parcelable { */ public native void scale(float scale, Region dst); - public final boolean union(Rect r) { + public final boolean union(@NonNull Rect r) { return op(r, Op.UNION); } @@ -259,7 +262,7 @@ public class Region implements Parcelable { * 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) { + public boolean op(@NonNull Rect r, @NonNull Op op) { return nativeOp(mNativeRegion, r.left, r.top, r.right, r.bottom, op.nativeInt); } @@ -268,7 +271,7 @@ public class Region implements Parcelable { * 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) { + public boolean op(int left, int top, int right, int bottom, @NonNull Op op) { return nativeOp(mNativeRegion, left, top, right, bottom, op.nativeInt); } @@ -277,7 +280,7 @@ public class Region implements Parcelable { * 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) { + public boolean op(@NonNull Region region, @NonNull Op op) { return op(this, region, op); } @@ -285,7 +288,7 @@ public class Region implements Parcelable { * 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) { + public boolean op(@NonNull Rect rect, @NonNull Region region, @NonNull Op op) { return nativeOp(mNativeRegion, rect, region.mNativeRegion, op.nativeInt); } @@ -294,11 +297,12 @@ public class Region implements Parcelable { * 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) { + public boolean op(@NonNull Region region1, @NonNull Region region2, @NonNull Op op) { return nativeOp(mNativeRegion, region1.mNativeRegion, region2.mNativeRegion, op.nativeInt); } + @Override public String toString() { return nativeToString(mNativeRegion); } @@ -308,6 +312,7 @@ public class Region implements Parcelable { * * @hide */ + @NonNull public static Region obtain() { Region region = sPool.acquire(); return (region != null) ? region : new Region(); @@ -320,7 +325,8 @@ public class Region implements Parcelable { * * @hide */ - public static Region obtain(Region other) { + @NonNull + public static Region obtain(@NonNull Region other) { Region region = obtain(); region.set(other); return region; @@ -346,6 +352,7 @@ public class Region implements Parcelable { * @param p Parcel object to read the region from * @return a new region created from the data in the parcel */ + @Override public Region createFromParcel(Parcel p) { long ni = nativeCreateFromParcel(p); if (ni == 0) { @@ -353,11 +360,13 @@ public class Region implements Parcelable { } return new Region(ni); } + @Override public Region[] newArray(int size) { return new Region[size]; } }; + @Override public int describeContents() { return 0; } @@ -367,6 +376,7 @@ public class Region implements Parcelable { * rebuilt from the parcel by calling CREATOR.createFromParcel(). * @param p Parcel object to write the region data into */ + @Override public void writeToParcel(Parcel p, int flags) { if (!nativeWriteToParcel(mNativeRegion, p)) { throw new RuntimeException(); @@ -382,6 +392,7 @@ public class Region implements Parcelable { return nativeEquals(mNativeRegion, peer.mNativeRegion); } + @Override protected void finalize() throws Throwable { try { nativeDestructor(mNativeRegion); diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java new file mode 100644 index 000000000000..45d7a2178b84 --- /dev/null +++ b/graphics/java/android/graphics/RenderNode.java @@ -0,0 +1,1482 @@ +/* + * 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.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.view.NativeVectorDrawableAnimator; +import android.view.RenderNodeAnimator; +import android.view.Surface; +import android.view.View; + +import dalvik.annotation.optimization.CriticalNative; +import dalvik.annotation.optimization.FastNative; + +import libcore.util.NativeAllocationRegistry; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * <p>RenderNode is used to build hardware accelerated rendering hierarchies. Each RenderNode + * contains both a display list as well as a set of properties that affect the rendering of the + * display list. RenderNodes are used internally for all Views by default and are not typically + * used directly.</p> + * + * <p>RenderNodes are used to divide up the rendering content of a complex scene into smaller + * pieces that can then be updated individually more cheaply. Updating part of the scene only needs + * to update the display list or properties of a small number of RenderNode instead of redrawing + * everything from scratch. A RenderNode only needs its display list re-recorded when its content + * alone should be changed. RenderNodes can also be transformed without re-recording the display + * list through the transform properties.</p> + * + * <p>A text editor might for instance store each paragraph into its own RenderNode. + * Thus when the user inserts or removes characters, only the display list of the + * affected paragraph needs to be recorded again.</p> + * + * <h3>Hardware acceleration</h3> + * <p>RenderNodes can be drawn using a {@link RecordingCanvas}. They are not + * supported in software. Always make sure that the {@link android.graphics.Canvas} + * you are using to render a display list is hardware accelerated using + * {@link android.graphics.Canvas#isHardwareAccelerated()}.</p> + * + * <h3>Creating a RenderNode</h3> + * <pre class="prettyprint"> + * RenderNode renderNode = RenderNode.create("myRenderNode"); + * renderNode.setLeftTopRightBottom(0, 0, 50, 50); // Set the size to 50x50 + * RecordingCanvas canvas = renderNode.startRecording(); + * try { + * // Draw with the canvas + * canvas.drawRect(...); + * } finally { + * renderNode.endRecording(); + * } + * </pre> + * + * <h3>Drawing a RenderNode in a View</h3> + * <pre class="prettyprint"> + * protected void onDraw(Canvas canvas) { + * if (canvas instanceof RecordingCanvas) { + * RecordingCanvas recordingCanvas = (RecordingCanvas) canvas; + * // Check that the RenderNode has a display list, re-recording it if it does not. + * if (!myRenderNode.hasDisplayList()) { + * updateDisplayList(myRenderNode); + * } + * // Draw the RenderNode into this canvas. + * recordingCanvas.drawRenderNode(myRenderNode); + * } + * } + * </pre> + * + * <h3>Releasing resources</h3> + * <p>This step is not mandatory but recommended if you want to release resources + * held by a display list as soon as possible. Most significantly any bitmaps it may contain.</p> + * <pre class="prettyprint"> + * // Discards the display list content allowing for any held resources to be released. + * // After calling this + * renderNode.discardDisplayList(); + * </pre> + * + * + * <h3>Properties</h3> + * <p>In addition, a RenderNode offers several properties, such as + * {@link #setScaleX(float)} or {@link #setLeft(int)}, that can be used to affect all + * the drawing commands recorded within. For instance, these properties can be used + * to move around a large number of images without re-issuing all the individual + * <code>canvas.drawBitmap()</code> calls.</p> + * + * <pre class="prettyprint"> + * private void createDisplayList() { + * mRenderNode = RenderNode.create("MyRenderNode"); + * mRenderNode.setLeftTopRightBottom(0, 0, width, height); + * RecordingCanvas canvas = mRenderNode.startRecording(); + * try { + * for (Bitmap b : mBitmaps) { + * canvas.drawBitmap(b, 0.0f, 0.0f, null); + * canvas.translate(0.0f, b.getHeight()); + * } + * } finally { + * mRenderNode.endRecording(); + * } + * } + * + * protected void onDraw(Canvas canvas) { + * if (canvas instanceof RecordingCanvas) { + * RecordingCanvas recordingCanvas = (RecordingCanvas) canvas; + * recordingCanvas.drawRenderNode(mRenderNode); + * } + * } + * + * private void moveContentBy(int x) { + * // This will move all the bitmaps recorded inside the display list + * // by x pixels to the right and redraw this view. All the commands + * // recorded in createDisplayList() won't be re-issued, only onDraw() + * // will be invoked and will execute very quickly + * mRenderNode.offsetLeftAndRight(x); + * invalidate(); + * } + * </pre> + * + * <h3>Threading</h3> + * <p>RenderNode may be created and used on any thread but they are not thread-safe. Only + * a single thread may interact with a RenderNode at any given time. It is critical + * that the RenderNode is only used on the same thread it is drawn with. For example when using + * RenderNode with a custom View, then that RenderNode must only be used from the UI thread.</p> + * + * <h3>When to re-render</h3> + * <p>Many of the RenderNode mutation methods, such as {@link #setTranslationX(float)}, return + * a boolean indicating if the value actually changed or not. This is useful in detecting + * if a new frame should be rendered or not. A typical usage would look like: + * <pre class="prettyprint"> + * public void translateTo(int x, int y) { + * boolean needsUpdate = myRenderNode.setTranslationX(x); + * needsUpdate |= myRenderNode.setTranslationY(y); + * if (needsUpdate) { + * myOwningView.invalidate(); + * } + * } + * </pre> + * This is marginally faster than doing a more explicit up-front check if the value changed by + * comparing the desired value against {@link #getTranslationX()} as it minimizes JNI transitions. + * The actual mechanism of requesting a new frame to be rendered will depend on how this + * RenderNode is being drawn. If it's drawn to a containing View, as in the above snippet, + * then simply invalidating that View works. If instead the RenderNode is being drawn to a Canvas + * directly such as with {@link Surface#lockHardwareCanvas()} then a new frame needs to be drawn + * by calling {@link Surface#lockHardwareCanvas()}, re-drawing the root RenderNode or whatever + * top-level content is desired, and finally calling {@link Surface#unlockCanvasAndPost(Canvas)}. + * </p> + */ +public final class RenderNode { + + // Use a Holder to allow static initialization in the boot image. + private static class NoImagePreloadHolder { + public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry( + RenderNode.class.getClassLoader(), nGetNativeFinalizer(), 1024); + } + + /** + * Not for general use; use only if you are ThreadedRenderer or RecordingCanvas. + * + * @hide + */ + public final long mNativeRenderNode; + private final AnimationHost mAnimationHost; + private RecordingCanvas mCurrentRecordingCanvas; + + /** + * Creates a new RenderNode that can be used to record batches of + * drawing operations, and store / apply render properties when drawn. + * + * @param name The name of the RenderNode, used for debugging purpose. May be null. + */ + public RenderNode(String name) { + this(name, null); + } + + private RenderNode(String name, AnimationHost animationHost) { + mNativeRenderNode = nCreate(name); + NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativeRenderNode); + mAnimationHost = animationHost; + } + + /** + * @see RenderNode#adopt(long) + */ + private RenderNode(long nativePtr) { + mNativeRenderNode = nativePtr; + NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativeRenderNode); + mAnimationHost = null; + } + + /** @hide */ + public static RenderNode create(String name, @Nullable AnimationHost animationHost) { + return new RenderNode(name, animationHost); + } + + /** + * Adopts an existing native render node. + * + * Note: This will *NOT* incRef() on the native object, however it will + * decRef() when it is destroyed. The caller should have already incRef'd it + * + * @hide + */ + public static RenderNode adopt(long nativePtr) { + return new RenderNode(nativePtr); + } + + /** + * Listens for RenderNode position updates for synchronous window movement. + * + * This is not suitable for generic position listening, it is only designed & intended + * for use by things which require external position events like SurfaceView, PopupWindow, etc.. + * + * @hide + */ + public interface PositionUpdateListener { + + /** + * Called by native by a Rendering Worker thread to update window position + * + * @hide + */ + void positionChanged(long frameNumber, int left, int top, int right, int bottom); + + /** + * Called by native on RenderThread to notify that the view is no longer in the + * draw tree. UI thread is blocked at this point. + * + * @hide + */ + void positionLost(long frameNumber); + + } + + /** + * Enable callbacks for position changes. + * + * @hide + */ + public void requestPositionUpdates(PositionUpdateListener listener) { + nRequestPositionUpdates(mNativeRenderNode, listener); + } + + + /** + * Starts recording a display list for the render node. All + * operations performed on the returned canvas are recorded and + * stored in this display list. + * + * {@link #endRecording()} must be called when the recording is finished in order to apply + * the updated display list. + * + * @param width The width of the recording viewport. This will not alter the width of the + * RenderNode itself, that must be set with {@link #setLeft(int)} and + * {@link #setRight(int)} + * @param height The height of the recording viewport. This will not alter the height of the + * RenderNode itself, that must be set with {@link #setTop(int)} and + * {@link #setBottom(int)}. + * @return A canvas to record drawing operations. + * @see #endRecording() + * @see #hasDisplayList() + */ + public RecordingCanvas startRecording(int width, int height) { + if (mCurrentRecordingCanvas != null) { + throw new IllegalStateException( + "Recording currently in progress - missing #endRecording() call?"); + } + mCurrentRecordingCanvas = RecordingCanvas.obtain(this, width, height); + return mCurrentRecordingCanvas; + } + + /** + * Same as {@link #startRecording(int, int)} with the width & height set + * to the RenderNode's own width & height. The RenderNode's width & height may be set + * with {@link #setLeftTopRightBottom(int, int, int, int)}. + */ + public RecordingCanvas startRecording() { + return startRecording(nGetWidth(mNativeRenderNode), nGetHeight(mNativeRenderNode)); + } + + /** + * @hide + * @deprecated use {@link #startRecording(int, int)} instead + */ + @Deprecated + public RecordingCanvas start(int width, int height) { + return startRecording(width, height); + } + + /** + * ` + * Ends the recording for this display list. Calling this method marks + * the display list valid and {@link #hasDisplayList()} will return true. + * + * @see #startRecording(int, int) + * @see #hasDisplayList() + */ + public void endRecording() { + if (mCurrentRecordingCanvas == null) { + throw new IllegalStateException( + "No recording in progress, forgot to call #startRecording()?"); + } + RecordingCanvas canvas = mCurrentRecordingCanvas; + mCurrentRecordingCanvas = null; + long displayList = canvas.finishRecording(); + nSetDisplayList(mNativeRenderNode, displayList); + canvas.recycle(); + } + + /** + * @hide + * @deprecated use {@link #endRecording()} instead + */ + @Deprecated + public void end(RecordingCanvas canvas) { + if (mCurrentRecordingCanvas != canvas) { + throw new IllegalArgumentException( + "Canvas given isn't the one that was returned from #startRecording"); + } + endRecording(); + } + + /** + * Reset native resources. This is called when cleaning up the state of display lists + * during destruction of hardware resources, to ensure that we do not hold onto + * obsolete resources after related resources are gone. + */ + public void discardDisplayList() { + nSetDisplayList(mNativeRenderNode, 0); + } + + /** + * Returns whether the RenderNode has a display list. If this returns false, the RenderNode + * should be re-recorded with {@link #startRecording()} and {@link #endRecording()}. + * + * A RenderNode without a display list may still be drawn, however it will have no impact + * on the rendering content until its display list is updated. + * + * When a RenderNode is no longer drawn by anything the system may automatically + * invoke {@link #discardDisplayList()}. It is therefore important to ensure that + * {@link #hasDisplayList()} is true on a RenderNode prior to drawing it. + * + * See {@link #discardDisplayList()} + * + * @return boolean true if this RenderNode has a display list, false otherwise. + */ + public boolean hasDisplayList() { + return nIsValid(mNativeRenderNode); + } + + /////////////////////////////////////////////////////////////////////////// + // Matrix manipulation + /////////////////////////////////////////////////////////////////////////// + + /** + * Whether or not the RenderNode has an identity transform. This is a faster + * way to do the otherwise equivalent {@link #getMatrix(Matrix)} {@link Matrix#isIdentity()} + * as it doesn't require copying the Matrix first, thus minimizing overhead. + * + * @return true if the RenderNode has an identity transform, false otherwise + */ + public boolean hasIdentityMatrix() { + return nHasIdentityMatrix(mNativeRenderNode); + } + + /** + * Gets the current transform matrix + * + * @param outMatrix The matrix to store the transform of the RenderNode + */ + public void getMatrix(@NonNull Matrix outMatrix) { + nGetTransformMatrix(mNativeRenderNode, outMatrix.native_instance); + } + + /** + * Gets the current transform inverted. This is a faster way to do the otherwise + * equivalent {@link #getMatrix(Matrix)} followed by {@link Matrix#invert(Matrix)} + * + * @param outMatrix The matrix to store the inverse transform of the RenderNode + */ + public void getInverseMatrix(@NonNull Matrix outMatrix) { + nGetInverseTransformMatrix(mNativeRenderNode, outMatrix.native_instance); + } + + /////////////////////////////////////////////////////////////////////////// + // RenderProperty Setters + /////////////////////////////////////////////////////////////////////////// + + /** + * @hide + * @deprecated use {@link #setUseCompositingLayer(boolean, Paint)} instead + */ + @Deprecated + public boolean setLayerType(int layerType) { + return nSetLayerType(mNativeRenderNode, layerType); + } + + /** + * @hide + * @deprecated use {@link #setUseCompositingLayer(boolean, Paint)} instead + */ + @Deprecated + public boolean setLayerPaint(@Nullable Paint paint) { + return nSetLayerPaint(mNativeRenderNode, paint != null ? paint.getNativeInstance() : 0); + } + + /** + * Controls whether or not to force this RenderNode to render to an intermediate buffer. + * Internally RenderNode will already promote itself to a composition layer if it's useful + * for performance or required for the current combination of {@link #setAlpha(float)} and + * {@link #setHasOverlappingRendering(boolean)}. + * + * The usage of this is instead to allow for either overriding of the internal behavior + * if it's measured to be necessary for the particular rendering content in question or, more + * usefully, to add a composition effect to the RenderNode via the optional paint parameter. + * + * Note: When a RenderNode is using a compositing layer it will also result in + * clipToBounds=true behavior. + * + * @param forceToLayer if true this forces the RenderNode to use an intermediate buffer. + * Default & generally recommended value is false. + * @param paint The blend mode, alpha, and ColorFilter to apply to the compositing layer. + * Only applies if forceToLayer is true. + * @return true if anything changed, false otherwise + */ + public boolean setUseCompositingLayer(boolean forceToLayer, @Nullable Paint paint) { + boolean didChange = nSetLayerType(mNativeRenderNode, forceToLayer ? 2 : 0); + didChange |= nSetLayerPaint(mNativeRenderNode, + paint != null ? paint.getNativeInstance() : 0); + return didChange; + } + + /** + * Sets the clip bounds of the RenderNode. + * + * @param rect the bounds to clip to. If null, the clip bounds are reset + * @return True if the clip bounds changed, false otherwise + */ + public boolean setClipBounds(@Nullable Rect rect) { + if (rect == null) { + return nSetClipBoundsEmpty(mNativeRenderNode); + } else { + return nSetClipBounds(mNativeRenderNode, rect.left, rect.top, rect.right, rect.bottom); + } + } + + /** + * Set whether the Render node should clip itself to its bounds. This property is controlled by + * the view's parent. + * + * @param clipToBounds true if the display list should clip to its bounds + */ + public boolean setClipToBounds(boolean clipToBounds) { + return nSetClipToBounds(mNativeRenderNode, clipToBounds); + } + + /** + * Sets whether the RenderNode should be drawn immediately after the + * closest ancestor RenderNode containing a projection receiver. + * + * @param shouldProject true if the display list should be projected onto a + * containing volume. + */ + public boolean setProjectBackwards(boolean shouldProject) { + return nSetProjectBackwards(mNativeRenderNode, shouldProject); + } + + /** + * Sets whether the RenderNode is a projection receiver - that its parent + * RenderNode should draw any descendent RenderNodes with + * ProjectBackwards=true directly on top of it. Default value is false. + */ + public boolean setProjectionReceiver(boolean shouldRecieve) { + return nSetProjectionReceiver(mNativeRenderNode, shouldRecieve); + } + + /** + * Sets the outline, defining the shape that casts a shadow, and the path to + * be clipped if setClipToOutline is set. + * + * This will make a copy of the provided {@link Outline}, so any future modifications + * to the outline will need to call {@link #setOutline(Outline)} with the modified + * outline for those changes to be applied. + * + * @param outline The outline to use for this RenderNode. + */ + public boolean setOutline(@Nullable Outline outline) { + if (outline == null) { + return nSetOutlineNone(mNativeRenderNode); + } + + switch (outline.mMode) { + case Outline.MODE_EMPTY: + return nSetOutlineEmpty(mNativeRenderNode); + case Outline.MODE_ROUND_RECT: + return nSetOutlineRoundRect(mNativeRenderNode, + outline.mRect.left, outline.mRect.top, + outline.mRect.right, outline.mRect.bottom, + outline.mRadius, outline.mAlpha); + case Outline.MODE_CONVEX_PATH: + return nSetOutlineConvexPath(mNativeRenderNode, outline.mPath.mNativePath, + outline.mAlpha); + } + + throw new IllegalArgumentException("Unrecognized outline?"); + } + + /** + * Checks if the RenderNode has a shadow. That is, if the combination of {@link #getElevation()} + * and {@link #getTranslationZ()} is greater than zero, there is an {@link Outline} set with + * a valid shadow caster path, and the provided outline has a non-zero + * {@link Outline#getAlpha()}. + * + * @return True if this RenderNode has a shadow, false otherwise + */ + public boolean hasShadow() { + return nHasShadow(mNativeRenderNode); + } + + /** + * Sets the color of the spot shadow that is drawn when the RenderNode has a positive Z or + * elevation value and is drawn inside of a {@link Canvas#enableZ()} section. + * <p> + * By default the shadow color is black. Generally, this color will be opaque so the intensity + * of the shadow is consistent between different RenderNodes with different colors. + * <p> + * The opacity of the final spot shadow is a function of the shadow caster height, the + * alpha channel of the outlineSpotShadowColor (typically opaque), and the + * {@link android.R.attr#spotShadowAlpha} theme attribute + * + * @param color The color this RenderNode will cast for its elevation spot shadow. + */ + public boolean setSpotShadowColor(int color) { + return nSetSpotShadowColor(mNativeRenderNode, color); + } + + /** + * @return The shadow color set by {@link #setSpotShadowColor(int)}, or black if nothing + * was set + */ + public int getSpotShadowColor() { + return nGetSpotShadowColor(mNativeRenderNode); + } + + /** + * Sets the color of the ambient shadow that is drawn when the RenderNode has a positive Z or + * elevation value and is drawn inside of a {@link Canvas#enableZ()} section. + * <p> + * By default the shadow color is black. Generally, this color will be opaque so the intensity + * of the shadow is consistent between different RenderNodes with different colors. + * <p> + * The opacity of the final ambient shadow is a function of the shadow caster height, the + * alpha channel of the outlineAmbientShadowColor (typically opaque), and the + * {@link android.R.attr#ambientShadowAlpha} theme attribute. + * + * @param color The color this RenderNode will cast for its elevation shadow. + */ + public boolean setAmbientShadowColor(int color) { + return nSetAmbientShadowColor(mNativeRenderNode, color); + } + + /** + * @return The shadow color set by {@link #setAmbientShadowColor(int)}, or black if + * nothing was set + */ + public int getAmbientShadowColor() { + return nGetAmbientShadowColor(mNativeRenderNode); + } + + /** + * Enables or disables clipping to the outline. + * + * @param clipToOutline true if clipping to the outline. + */ + public boolean setClipToOutline(boolean clipToOutline) { + return nSetClipToOutline(mNativeRenderNode, clipToOutline); + } + + /** + * See {@link #setClipToOutline(boolean)} + * + * @return True if this RenderNode clips to its outline, false otherwise + */ + public boolean getClipToOutline() { + return nGetClipToOutline(mNativeRenderNode); + } + + /** + * Controls the RenderNode's circular reveal clip. + * + * @hide + */ + public boolean setRevealClip(boolean shouldClip, + float x, float y, float radius) { + return nSetRevealClip(mNativeRenderNode, shouldClip, x, y, radius); + } + + /** + * Set the static matrix on the display list. The specified matrix is combined with other + * transforms (such as {@link #setScaleX(float)}, {@link #setRotation(float)}, etc.) + * + * @param matrix A transform matrix to apply to this display list + * @hide TODO Do we want this? + */ + public boolean setStaticMatrix(Matrix matrix) { + return nSetStaticMatrix(mNativeRenderNode, matrix.native_instance); + } + + /** + * Set the Animation matrix on the display list. This matrix exists if an Animation is + * currently playing on a View, and is set on the display list during at draw() time. When + * the Animation finishes, the matrix should be cleared by sending <code>null</code> + * for the matrix parameter. + * + * @param matrix The matrix, null indicates that the matrix should be cleared. + * @hide TODO Do we want this? + */ + public boolean setAnimationMatrix(Matrix matrix) { + return nSetAnimationMatrix(mNativeRenderNode, + (matrix != null) ? matrix.native_instance : 0); + } + + /** + * Sets the translucency level for the display list. + * + * @param alpha The translucency of the display list, must be a value between 0.0f and 1.0f + * @see View#setAlpha(float) + * @see #getAlpha() + */ + public boolean setAlpha(float alpha) { + return nSetAlpha(mNativeRenderNode, alpha); + } + + /** + * Returns the translucency level of this display list. + * + * @return A value between 0.0f and 1.0f + * @see #setAlpha(float) + */ + public float getAlpha() { + return nGetAlpha(mNativeRenderNode); + } + + /** + * Sets whether the display list renders content which overlaps. Non-overlapping rendering + * can use a fast path for alpha that avoids rendering to an offscreen buffer. By default + * display lists consider they do not have overlapping content. + * + * @param hasOverlappingRendering False if the content is guaranteed to be non-overlapping, + * true otherwise. + * @see android.view.View#hasOverlappingRendering() + * @see #hasOverlappingRendering() + */ + public boolean setHasOverlappingRendering(boolean hasOverlappingRendering) { + return nSetHasOverlappingRendering(mNativeRenderNode, hasOverlappingRendering); + } + + /** @hide */ + @IntDef({USAGE_BACKGROUND}) + @Retention(RetentionPolicy.SOURCE) + public @interface UsageHint { + } + + /** + * The default usage hint + * + * @hide + */ + public static final int USAGE_UNKNOWN = 0; + + /** + * Usage is background content + * + * @hide + */ + public static final int USAGE_BACKGROUND = 1; + + /** + * Provides a hint on what this RenderNode's display list content contains. This hint is used + * for automatic content transforms to improve accessibility or similar. + * + * @hide + */ + public void setUsageHint(@UsageHint int usageHint) { + nSetUsageHint(mNativeRenderNode, usageHint); + } + + /** + * Indicates whether the content of this display list overlaps. + * + * @return True if this display list renders content which overlaps, false otherwise. + * @see #setHasOverlappingRendering(boolean) + */ + public boolean hasOverlappingRendering() { + return nHasOverlappingRendering(mNativeRenderNode); + } + + /** + * Sets the base elevation of this RenderNode in pixels + * + * @param lift the elevation in pixels + * @return true if the elevation changed, false if it was the same + */ + public boolean setElevation(float lift) { + return nSetElevation(mNativeRenderNode, lift); + } + + /** + * See {@link #setElevation(float)} + * + * @return The RenderNode's current elevation + */ + public float getElevation() { + return nGetElevation(mNativeRenderNode); + } + + /** + * Sets the translation value for the display list on the X axis. + * + * @param translationX The X axis translation value of the display list, in pixels + * @see View#setTranslationX(float) + * @see #getTranslationX() + */ + public boolean setTranslationX(float translationX) { + return nSetTranslationX(mNativeRenderNode, translationX); + } + + /** + * Returns the translation value for this display list on the X axis, in pixels. + * + * @see #setTranslationX(float) + */ + public float getTranslationX() { + return nGetTranslationX(mNativeRenderNode); + } + + /** + * Sets the translation value for the display list on the Y axis. + * + * @param translationY The Y axis translation value of the display list, in pixels + * @see View#setTranslationY(float) + * @see #getTranslationY() + */ + public boolean setTranslationY(float translationY) { + return nSetTranslationY(mNativeRenderNode, translationY); + } + + /** + * Returns the translation value for this display list on the Y axis, in pixels. + * + * @see #setTranslationY(float) + */ + public float getTranslationY() { + return nGetTranslationY(mNativeRenderNode); + } + + /** + * Sets the translation value for the display list on the Z axis. + * + * @see View#setTranslationZ(float) + * @see #getTranslationZ() + */ + public boolean setTranslationZ(float translationZ) { + return nSetTranslationZ(mNativeRenderNode, translationZ); + } + + /** + * Returns the translation value for this display list on the Z axis. + * + * @see #setTranslationZ(float) + */ + public float getTranslationZ() { + return nGetTranslationZ(mNativeRenderNode); + } + + /** + * Sets the rotation value for the display list around the Z axis. + * + * @param rotation The rotation value of the display list, in degrees + * @see View#setRotation(float) + * @see #getRotation() + */ + public boolean setRotation(float rotation) { + return nSetRotation(mNativeRenderNode, rotation); + } + + /** + * Returns the rotation value for this display list around the Z axis, in degrees. + * + * @see #setRotation(float) + */ + public float getRotation() { + return nGetRotation(mNativeRenderNode); + } + + /** + * Sets the rotation value for the display list around the X axis. + * + * @param rotationX The rotation value of the display list, in degrees + * @see View#setRotationX(float) + * @see #getRotationX() + */ + public boolean setRotationX(float rotationX) { + return nSetRotationX(mNativeRenderNode, rotationX); + } + + /** + * Returns the rotation value for this display list around the X axis, in degrees. + * + * @see #setRotationX(float) + */ + public float getRotationX() { + return nGetRotationX(mNativeRenderNode); + } + + /** + * Sets the rotation value for the display list around the Y axis. + * + * @param rotationY The rotation value of the display list, in degrees + * @see View#setRotationY(float) + * @see #getRotationY() + */ + public boolean setRotationY(float rotationY) { + return nSetRotationY(mNativeRenderNode, rotationY); + } + + /** + * Returns the rotation value for this display list around the Y axis, in degrees. + * + * @see #setRotationY(float) + */ + public float getRotationY() { + return nGetRotationY(mNativeRenderNode); + } + + /** + * Sets the scale value for the display list on the X axis. + * + * @param scaleX The scale value of the display list + * @see View#setScaleX(float) + * @see #getScaleX() + */ + public boolean setScaleX(float scaleX) { + return nSetScaleX(mNativeRenderNode, scaleX); + } + + /** + * Returns the scale value for this display list on the X axis. + * + * @see #setScaleX(float) + */ + public float getScaleX() { + return nGetScaleX(mNativeRenderNode); + } + + /** + * Sets the scale value for the display list on the Y axis. + * + * @param scaleY The scale value of the display list + * @see View#setScaleY(float) + * @see #getScaleY() + */ + public boolean setScaleY(float scaleY) { + return nSetScaleY(mNativeRenderNode, scaleY); + } + + /** + * Returns the scale value for this display list on the Y axis. + * + * @see #setScaleY(float) + */ + public float getScaleY() { + return nGetScaleY(mNativeRenderNode); + } + + /** + * Sets the pivot value for the display list on the X axis + * + * @param pivotX The pivot value of the display list on the X axis, in pixels + * @see View#setPivotX(float) + * @see #getPivotX() + */ + public boolean setPivotX(float pivotX) { + return nSetPivotX(mNativeRenderNode, pivotX); + } + + /** + * Returns the pivot value for this display list on the X axis, in pixels. + * + * @see #setPivotX(float) + */ + public float getPivotX() { + return nGetPivotX(mNativeRenderNode); + } + + /** + * Sets the pivot value for the display list on the Y axis + * + * @param pivotY The pivot value of the display list on the Y axis, in pixels + * @see View#setPivotY(float) + * @see #getPivotY() + */ + public boolean setPivotY(float pivotY) { + return nSetPivotY(mNativeRenderNode, pivotY); + } + + /** + * Returns the pivot value for this display list on the Y axis, in pixels. + * + * @see #setPivotY(float) + */ + public float getPivotY() { + return nGetPivotY(mNativeRenderNode); + } + + /** + * @return Whether or not a pivot was explicitly set with {@link #setPivotX(float)} or + * {@link #setPivotY(float)}. If no pivot has been set then the pivot will be the center + * of the RenderNode. + */ + public boolean isPivotExplicitlySet() { + return nIsPivotExplicitlySet(mNativeRenderNode); + } + + /** + * Clears any pivot previously set by a call to {@link #setPivotX(float)} or + * {@link #setPivotY(float)}. After calling this {@link #isPivotExplicitlySet()} will be false + * and the pivot used for rotation will return to default of being centered on the view. + */ + public boolean resetPivot() { + return nResetPivot(mNativeRenderNode); + } + + /** + * <p>Sets the distance along the Z axis (orthogonal to the X/Y plane on which + * RenderNodes are drawn) from the camera to this RenderNode. The camera's distance + * affects 3D transformations, for instance rotations around the X and Y + * axis. If the rotationX or rotationY properties are changed and this view is + * large (more than half the size of the screen), it is recommended to always + * use a camera distance that's greater than the height (X axis rotation) or + * the width (Y axis rotation) of this view.</p> + * + * <p>The distance of the camera from the drawing plane can have an affect on the + * perspective distortion of the RenderNode when it is rotated around the x or y axis. + * For example, a large distance will result in a large viewing angle, and there + * will not be much perspective distortion of the view as it rotates. A short + * distance may cause much more perspective distortion upon rotation, and can + * also result in some drawing artifacts if the rotated view ends up partially + * behind the camera (which is why the recommendation is to use a distance at + * least as far as the size of the view, if the view is to be rotated.)</p> + * + * <p>The distance is expressed in pixels and must always be positive</p> + * + * @param distance The distance in pixels, must always be positive + * @see #setRotationX(float) + * @see #setRotationY(float) + */ + public boolean setCameraDistance(float distance) { + if (!Float.isFinite(distance) || distance < 0.0f) { + throw new IllegalArgumentException("distance must be finite & positive, given=" + + distance); + } + // Native actually wants this to be negative not positive, so we flip it. + return nSetCameraDistance(mNativeRenderNode, -distance); + } + + /** + * Returns the distance in Z of the camera for this RenderNode + * + * @return the distance along the Z axis in pixels. + * @see #setCameraDistance(float) + */ + public float getCameraDistance() { + return -nGetCameraDistance(mNativeRenderNode); + } + + /** + * Sets the left position for the RenderNode. + * + * @param left The left position, in pixels, of the RenderNode + * @return true if the value changed, false otherwise + */ + public boolean setLeft(int left) { + return nSetLeft(mNativeRenderNode, left); + } + + /** + * Sets the top position for the RenderNode. + * + * @param top The top position, in pixels, of the RenderNode + * @return true if the value changed, false otherwise. + */ + public boolean setTop(int top) { + return nSetTop(mNativeRenderNode, top); + } + + /** + * Sets the right position for the RenderNode. + * + * @param right The right position, in pixels, of the RenderNode + * @return true if the value changed, false otherwise. + */ + public boolean setRight(int right) { + return nSetRight(mNativeRenderNode, right); + } + + /** + * Sets the bottom position for the RenderNode. + * + * @param bottom The bottom position, in pixels, of the RenderNode + * @return true if the value changed, false otherwise. + */ + public boolean setBottom(int bottom) { + return nSetBottom(mNativeRenderNode, bottom); + } + + /** + * Gets the left position for the RenderNode. + * + * See {@link #setLeft(int)} + * + * @return the left position in pixels + */ + public int getLeft() { + return nGetLeft(mNativeRenderNode); + } + + /** + * Gets the top position for the RenderNode. + * + * See {@link #setTop(int)} + * + * @return the top position in pixels + */ + public int getTop() { + return nGetTop(mNativeRenderNode); + } + + /** + * Gets the right position for the RenderNode. + * + * See {@link #setRight(int)} + * + * @return the right position in pixels + */ + public int getRight() { + return nGetRight(mNativeRenderNode); + } + + /** + * Gets the bottom position for the RenderNode. + * + * See {@link #setBottom(int)} + * + * @return the bottom position in pixels + */ + public int getBottom() { + return nGetBottom(mNativeRenderNode); + } + + /** + * Gets the width of the RenderNode, which is the right - left. + * + * @return the width of the RenderNode + */ + public int getWidth() { + return nGetWidth(mNativeRenderNode); + } + + /** + * Gets the height of the RenderNode, which is the bottom - top. + * + * @return the height of the RenderNode + */ + public int getHeight() { + return nGetHeight(mNativeRenderNode); + } + + /** + * Sets the left, top, right, and bottom of the RenderNode. + * + * @param left The left position of the RenderNode, in pixels + * @param top The top position of the RenderNode, in pixels + * @param right The right position of the RenderNode, in pixels + * @param bottom The bottom position of the RenderNode, in pixels + * @return true if any values changed, false otherwise. + * @see #setLeft(int) + * @see #setTop(int) + * @see #setRight(int) + * @see #setBottom(int) + */ + public boolean setLeftTopRightBottom(int left, int top, int right, int bottom) { + return nSetLeftTopRightBottom(mNativeRenderNode, left, top, right, bottom); + } + + /** + * Offsets the left and right positions for the RenderNode + * + * @param offset The amount that the left and right positions are offset in pixels + * @return true if any values changed, false otherwise. + */ + public boolean offsetLeftAndRight(int offset) { + return nOffsetLeftAndRight(mNativeRenderNode, offset); + } + + /** + * Offsets the top and bottom values for the RenderNode + * + * @param offset The amount that the left and right positions are offset in pixels + * @return true if any values changed, false otherwise. + */ + public boolean offsetTopAndBottom(int offset) { + return nOffsetTopAndBottom(mNativeRenderNode, offset); + } + + /** + * Outputs the RenderNode to the log. This method exists for use by + * tools to output display lists for selected nodes to the log. + * + * @hide TODO: Expose? Should the shape of this be different than forced dump to logcat? + */ + public void output() { + nOutput(mNativeRenderNode); + } + + /** + * Gets the approximate memory usage of the RenderNode for debug purposes. Does not include + * the memory usage of any child RenderNodes nor any bitmaps, only the memory usage of + * this RenderNode and any data it owns. + */ + public int computeApproximateMemoryUsage() { + return nGetDebugSize(mNativeRenderNode); + } + + /** + * Sets whether or not to allow force dark to apply to this RenderNode. + * + * Setting this to false will disable the auto-dark feature on everything this RenderNode + * draws, including any descendants. + * + * Setting this to true will allow this RenderNode to be automatically made dark, however + * a value of 'true' will not override any 'false' value in its parent chain nor will + * it prevent any 'false' in any of its children. + * + * @param allow Whether or not to allow force dark. + * @return true If the value has changed, false otherwise. + */ + public boolean setForceDarkAllowed(boolean allow) { + return nSetAllowForceDark(mNativeRenderNode, allow); + } + + /** + * See {@link #setForceDarkAllowed(boolean)} + * + * @return true if force dark is allowed (default), false if it is disabled + */ + public boolean isForceDarkAllowed() { + return nGetAllowForceDark(mNativeRenderNode); + } + + /////////////////////////////////////////////////////////////////////////// + // Animations + /////////////////////////////////////////////////////////////////////////// + + /** + * TODO: Figure out if this can be eliminated/refactored away + * + * For now this interface exists to de-couple RenderNode from anything View-specific in a + * bit of a kludge. + * + * @hide + */ + public interface AnimationHost { + /** @hide */ + void registerAnimatingRenderNode(RenderNode animator); + + /** @hide */ + void registerVectorDrawableAnimator(NativeVectorDrawableAnimator animator); + + /** @hide */ + boolean isAttached(); + } + + /** @hide */ + public void addAnimator(RenderNodeAnimator animator) { + if (!isAttached()) { + throw new IllegalStateException("Cannot start this animator on a detached view!"); + } + nAddAnimator(mNativeRenderNode, animator.getNativeAnimator()); + mAnimationHost.registerAnimatingRenderNode(this); + } + + /** @hide */ + public boolean isAttached() { + return mAnimationHost != null && mAnimationHost.isAttached(); + } + + /** @hide */ + public void registerVectorDrawableAnimator(NativeVectorDrawableAnimator animatorSet) { + if (!isAttached()) { + throw new IllegalStateException("Cannot start this animator on a detached view!"); + } + mAnimationHost.registerVectorDrawableAnimator(animatorSet); + } + + /** @hide */ + public void endAllAnimators() { + nEndAllAnimators(mNativeRenderNode); + } + + /////////////////////////////////////////////////////////////////////////// + // Regular JNI methods + /////////////////////////////////////////////////////////////////////////// + + private static native long nCreate(String name); + + private static native long nGetNativeFinalizer(); + + private static native void nOutput(long renderNode); + + private static native int nGetDebugSize(long renderNode); + + private static native void nRequestPositionUpdates(long renderNode, + PositionUpdateListener callback); + + // Animations + + private static native void nAddAnimator(long renderNode, long animatorPtr); + + private static native void nEndAllAnimators(long renderNode); + + + /////////////////////////////////////////////////////////////////////////// + // @FastNative methods + /////////////////////////////////////////////////////////////////////////// + + @FastNative + private static native void nSetDisplayList(long renderNode, long newData); + + + /////////////////////////////////////////////////////////////////////////// + // @CriticalNative methods + /////////////////////////////////////////////////////////////////////////// + + @CriticalNative + private static native boolean nIsValid(long renderNode); + + // Matrix + + @CriticalNative + private static native void nGetTransformMatrix(long renderNode, long nativeMatrix); + + @CriticalNative + private static native void nGetInverseTransformMatrix(long renderNode, long nativeMatrix); + + @CriticalNative + private static native boolean nHasIdentityMatrix(long renderNode); + + // Properties + + @CriticalNative + private static native boolean nOffsetTopAndBottom(long renderNode, int offset); + + @CriticalNative + private static native boolean nOffsetLeftAndRight(long renderNode, int offset); + + @CriticalNative + private static native boolean nSetLeftTopRightBottom(long renderNode, int left, int top, + int right, int bottom); + + @CriticalNative + private static native boolean nSetLeft(long renderNode, int left); + + @CriticalNative + private static native boolean nSetTop(long renderNode, int top); + + @CriticalNative + private static native boolean nSetRight(long renderNode, int right); + + @CriticalNative + private static native boolean nSetBottom(long renderNode, int bottom); + + @CriticalNative + private static native int nGetLeft(long renderNode); + + @CriticalNative + private static native int nGetTop(long renderNode); + + @CriticalNative + private static native int nGetRight(long renderNode); + + @CriticalNative + private static native int nGetBottom(long renderNode); + + @CriticalNative + private static native boolean nSetCameraDistance(long renderNode, float distance); + + @CriticalNative + private static native boolean nSetPivotY(long renderNode, float pivotY); + + @CriticalNative + private static native boolean nSetPivotX(long renderNode, float pivotX); + + @CriticalNative + private static native boolean nResetPivot(long renderNode); + + @CriticalNative + private static native boolean nSetLayerType(long renderNode, int layerType); + + @CriticalNative + private static native boolean nSetLayerPaint(long renderNode, long paint); + + @CriticalNative + private static native boolean nSetClipToBounds(long renderNode, boolean clipToBounds); + + @CriticalNative + private static native boolean nSetClipBounds(long renderNode, int left, int top, + int right, int bottom); + + @CriticalNative + private static native boolean nSetClipBoundsEmpty(long renderNode); + + @CriticalNative + private static native boolean nSetProjectBackwards(long renderNode, boolean shouldProject); + + @CriticalNative + private static native boolean nSetProjectionReceiver(long renderNode, boolean shouldRecieve); + + @CriticalNative + private static native boolean nSetOutlineRoundRect(long renderNode, int left, int top, + int right, int bottom, float radius, float alpha); + + @CriticalNative + private static native boolean nSetOutlineConvexPath(long renderNode, long nativePath, + float alpha); + + @CriticalNative + private static native boolean nSetOutlineEmpty(long renderNode); + + @CriticalNative + private static native boolean nSetOutlineNone(long renderNode); + + @CriticalNative + private static native boolean nHasShadow(long renderNode); + + @CriticalNative + private static native boolean nSetSpotShadowColor(long renderNode, int color); + + @CriticalNative + private static native boolean nSetAmbientShadowColor(long renderNode, int color); + + @CriticalNative + private static native int nGetSpotShadowColor(long renderNode); + + @CriticalNative + private static native int nGetAmbientShadowColor(long renderNode); + + @CriticalNative + private static native boolean nSetClipToOutline(long renderNode, boolean clipToOutline); + + @CriticalNative + private static native boolean nSetRevealClip(long renderNode, + boolean shouldClip, float x, float y, float radius); + + @CriticalNative + private static native boolean nSetAlpha(long renderNode, float alpha); + + @CriticalNative + private static native boolean nSetHasOverlappingRendering(long renderNode, + boolean hasOverlappingRendering); + + @CriticalNative + private static native void nSetUsageHint(long renderNode, int usageHint); + + @CriticalNative + private static native boolean nSetElevation(long renderNode, float lift); + + @CriticalNative + private static native boolean nSetTranslationX(long renderNode, float translationX); + + @CriticalNative + private static native boolean nSetTranslationY(long renderNode, float translationY); + + @CriticalNative + private static native boolean nSetTranslationZ(long renderNode, float translationZ); + + @CriticalNative + private static native boolean nSetRotation(long renderNode, float rotation); + + @CriticalNative + private static native boolean nSetRotationX(long renderNode, float rotationX); + + @CriticalNative + private static native boolean nSetRotationY(long renderNode, float rotationY); + + @CriticalNative + private static native boolean nSetScaleX(long renderNode, float scaleX); + + @CriticalNative + private static native boolean nSetScaleY(long renderNode, float scaleY); + + @CriticalNative + private static native boolean nSetStaticMatrix(long renderNode, long nativeMatrix); + + @CriticalNative + private static native boolean nSetAnimationMatrix(long renderNode, long animationMatrix); + + @CriticalNative + private static native boolean nHasOverlappingRendering(long renderNode); + + @CriticalNative + private static native boolean nGetClipToOutline(long renderNode); + + @CriticalNative + private static native float nGetAlpha(long renderNode); + + @CriticalNative + private static native float nGetCameraDistance(long renderNode); + + @CriticalNative + private static native float nGetScaleX(long renderNode); + + @CriticalNative + private static native float nGetScaleY(long renderNode); + + @CriticalNative + private static native float nGetElevation(long renderNode); + + @CriticalNative + private static native float nGetTranslationX(long renderNode); + + @CriticalNative + private static native float nGetTranslationY(long renderNode); + + @CriticalNative + private static native float nGetTranslationZ(long renderNode); + + @CriticalNative + private static native float nGetRotation(long renderNode); + + @CriticalNative + private static native float nGetRotationX(long renderNode); + + @CriticalNative + private static native float nGetRotationY(long renderNode); + + @CriticalNative + private static native boolean nIsPivotExplicitlySet(long renderNode); + + @CriticalNative + private static native float nGetPivotX(long renderNode); + + @CriticalNative + private static native float nGetPivotY(long renderNode); + + @CriticalNative + private static native int nGetWidth(long renderNode); + + @CriticalNative + private static native int nGetHeight(long renderNode); + + @CriticalNative + private static native boolean nSetAllowForceDark(long renderNode, boolean allowForceDark); + + @CriticalNative + private static native boolean nGetAllowForceDark(long renderNode); +} diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index 5b4e327f7caf..bf969ef15c5b 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -27,15 +27,16 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; import android.content.res.AssetManager; +import android.graphics.fonts.Font; +import android.graphics.fonts.FontFamily; +import android.graphics.fonts.FontStyle; import android.graphics.fonts.FontVariationAxis; -import android.net.Uri; +import android.graphics.fonts.SystemFonts; import android.os.Build; 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; @@ -48,18 +49,12 @@ import dalvik.annotation.optimization.CriticalNative; import libcore.util.NativeAllocationRegistry; -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.io.InputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -122,9 +117,15 @@ public class Typeface { private static final Object sDynamicCacheLock = new Object(); static Typeface sDefaultTypeface; + + // Following two fields are not used but left for hiddenapi private list @UnsupportedAppUsage static final Map<String, Typeface> sSystemFontMap; - static final Map<String, FontFamily[]> sSystemFallbackMap; + + // We cannot support sSystemFallbackMap since we will migrate to public FontFamily API. + @UnsupportedAppUsage + static final Map<String, android.graphics.FontFamily[]> sSystemFallbackMap = + Collections.emptyMap(); /** * @hide @@ -147,13 +148,7 @@ public class Typeface { @UnsupportedAppUsage private @Style int mStyle = 0; - /** - * A maximum value for the weight value. - * @hide - */ - public static final int MAX_WEIGHT = 1000; - - private @IntRange(from = 0, to = MAX_WEIGHT) int mWeight = 0; + private @IntRange(from = 0, to = FontStyle.FONT_WEIGHT_MAX) 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 @@ -168,6 +163,9 @@ public class Typeface { private int[] mSupportedAxes; private static final int[] EMPTY_AXES = {}; + // The underlying font families. + private final FontFamily[] mFamilies; + @UnsupportedAppUsage private static void setDefault(Typeface t) { sDefaultTypeface = t; @@ -196,38 +194,6 @@ public class Typeface { /** * @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 @@ -262,21 +228,34 @@ public class Typeface { // family is FontFamilyFilesResourceEntry final FontFamilyFilesResourceEntry filesEntry = (FontFamilyFilesResourceEntry) entry; - FontFamily fontFamily = new FontFamily(); - for (final FontFileResourceEntry fontFile : filesEntry.getEntries()) { - if (!fontFamily.addFontFromAssetManager(mgr, fontFile.getFileName(), - 0 /* resourceCookie */, false /* isAsset */, fontFile.getTtcIndex(), - fontFile.getWeight(), fontFile.getItalic(), - FontVariationAxis.fromFontVariationSettings(fontFile.getVariationSettings()))) { - return null; + try { + FontFamily.Builder familyBuilder = null; + for (final FontFileResourceEntry fontFile : filesEntry.getEntries()) { + final Font.Builder fontBuilder = new Font.Builder(mgr, fontFile.getFileName(), + false /* isAsset */, 0 /* cookie */) + .setTtcIndex(fontFile.getTtcIndex()) + .setFontVariationSettings(fontFile.getVariationSettings()); + if (fontFile.getWeight() != Typeface.RESOLVE_BY_FONT_TABLE) { + fontBuilder.setWeight(fontFile.getWeight()); + } + if (fontFile.getItalic() != Typeface.RESOLVE_BY_FONT_TABLE) { + fontBuilder.setSlant(fontFile.getItalic() == FontFileResourceEntry.ITALIC + ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT); + } + + if (familyBuilder == null) { + familyBuilder = new FontFamily.Builder(fontBuilder.build()); + } else { + familyBuilder.addFont(fontBuilder.build()); + } } + if (familyBuilder == null) { + return Typeface.DEFAULT; + } + typeface = new Typeface.CustomFallbackBuilder(familyBuilder.build()).build(); + } catch (IOException e) { + typeface = Typeface.DEFAULT; } - 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 */, @@ -343,15 +322,11 @@ public class Typeface { /** @hide */ public static final int BOLD_WEIGHT = 700; - private int mTtcIndex; - private FontVariationAxis[] mAxes; - - private AssetManager mAssetManager; - private String mPath; - private FileDescriptor mFd; + // Kept for generating asset cache key. + private final AssetManager mAssetManager; + private final String mPath; - private FontsContract.FontInfo[] mFonts; - private Map<Uri, ByteBuffer> mFontBuffers; + private final Font.Builder mFontBuilder; private String mFallbackFamilyName; @@ -364,7 +339,9 @@ public class Typeface { * @param path The file object refers to the font file. */ public Builder(@NonNull File path) { - mPath = path.getAbsolutePath(); + mFontBuilder = new Font.Builder(path); + mAssetManager = null; + mPath = null; } /** @@ -376,7 +353,9 @@ public class Typeface { * @param fd The file descriptor. The passed fd must be mmap-able. */ public Builder(@NonNull FileDescriptor fd) { - mFd = fd; + mFontBuilder = new Font.Builder(fd); + mAssetManager = null; + mPath = null; } /** @@ -385,7 +364,9 @@ public class Typeface { * @param path The full path to the font file. */ public Builder(@NonNull String path) { - mPath = path; + mFontBuilder = new Font.Builder(new File(path)); + mAssetManager = null; + mPath = null; } /** @@ -395,27 +376,22 @@ public class Typeface { * @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); + this(assetManager, path, true /* is asset */, 0 /* cookie */); } /** - * 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. + * Constructs a builder from an asset manager and a file path in an asset directory. * - * @param fonts The array of {@link FontsContract.FontInfo} - * @param buffers The mapping from URI to buffers to be used during building. + * @param assetManager The application's asset manager + * @param path The file name of the font data in the asset directory + * @param cookie a cookie for the asset * @hide */ - public Builder(@NonNull FontsContract.FontInfo[] fonts, - @NonNull Map<Uri, ByteBuffer> buffers) { - mFonts = fonts; - mFontBuffers = buffers; + public Builder(@NonNull AssetManager assetManager, @NonNull String path, boolean isAsset, + int cookie) { + mFontBuilder = new Font.Builder(assetManager, path, isAsset, cookie); + mAssetManager = assetManager; + mPath = path; } /** @@ -427,6 +403,7 @@ public class Typeface { */ public Builder setWeight(@IntRange(from = 1, to = 1000) int weight) { mWeight = weight; + mFontBuilder.setWeight(weight); return this; } @@ -438,7 +415,8 @@ public class Typeface { * @param italic {@code true} if the font is italic. Otherwise {@code false}. */ public Builder setItalic(boolean italic) { - mItalic = italic ? STYLE_ITALIC : STYLE_NORMAL; + mItalic = italic ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT; + mFontBuilder.setSlant(mItalic); return this; } @@ -450,11 +428,7 @@ public class Typeface { * 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; + mFontBuilder.setTtcIndex(ttcIndex); return this; } @@ -466,14 +440,7 @@ public class Typeface { * 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); + mFontBuilder.setFontVariationSettings(variationSettings); return this; } @@ -483,14 +450,7 @@ public class Typeface { * @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; + mFontBuilder.setFontVariationSettings(axes); return this; } @@ -566,11 +526,7 @@ public class Typeface { return null; } - Typeface base = sSystemFontMap.get(mFallbackFamilyName); - if (base == null) { - base = sDefaultTypeface; - } - + final Typeface base = getSystemDefaultTypeface(mFallbackFamilyName); if (mWeight == RESOLVE_BY_FONT_TABLE && mItalic == RESOLVE_BY_FONT_TABLE) { return base; } @@ -587,91 +543,198 @@ public class Typeface { * @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, + try { + final Font font = mFontBuilder.build(); + final String key = mAssetManager == null ? null : createAssetUid( + mAssetManager, mPath, font.getTtcIndex(), font.getAxes(), + font.getStyle().getWeight(), font.getStyle().getSlant(), 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(); + if (key != null) { + // Dynamic cache lookup is only for assets. + synchronized (sDynamicCacheLock) { + final Typeface typeface = sDynamicTypefaceCache.get(key); + if (typeface != null) { + return typeface; + } } - 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(); + final FontFamily family = new FontFamily.Builder(font).build(); + final int weight = mWeight == RESOLVE_BY_FONT_TABLE + ? font.getStyle().getWeight() : mWeight; + final int slant = mItalic == RESOLVE_BY_FONT_TABLE + ? font.getStyle().getSlant() : mItalic; + final CustomFallbackBuilder builder = new CustomFallbackBuilder(family) + .setStyle(new FontStyle(weight, slant)); + if (mFallbackFamilyName != null) { + builder.setSystemFallback(mFallbackFamilyName); } - 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; + final Typeface typeface = builder.build(); + if (key != null) { + synchronized (sDynamicCacheLock) { + sDynamicTypefaceCache.put(key, typeface); } - 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); + return typeface; + } catch (IOException | IllegalArgumentException e) { + return resolveFallbackTypeface(); } + } + } - // Must not reach here. - throw new IllegalArgumentException("No source was set."); + /** + * A builder class for creating new Typeface instance. + * + * There are two font fallback mechanisms, custom font fallback and system font fallback. + * The custom font fallback is a simple ordered list. The text renderer tries to see if it can + * render a character with the first font and if that font does not support the character, try + * next one and so on. It will keep trying until end of the custom fallback chain. The maximum + * length of the custom fallback chain is 64. + * The system font fallback is a system pre-defined fallback chain. The system fallback is + * processed only when no matching font is found in the custom font fallback. + * + * <p> + * Examples, + * 1) Create Typeface from single ttf file. + * <pre> + * <code> + * Font font = new Font.Builder("your_font_file.ttf").build(); + * FontFamily family = new FontFamily.Builder(font).build(); + * Typeface typeface = new Typeface.CustomFallbackBuilder(family).build(); + * </code> + * </pre> + * + * 2) Create Typeface from multiple font files and select bold style by default. + * <pre> + * <code> + * Font regularFont = new Font.Builder("regular.ttf").build(); + * Font boldFont = new Font.Builder("bold.ttf").build(); + * FontFamily family = new FontFamily.Builder(regularFont) + * .addFont(boldFont).build(); + * Typeface typeface = new Typeface.CustomFallbackBuilder(family) + * .setWeight(Font.FONT_WEIGHT_BOLD) // Set bold style as the default style. + * // If the font family doesn't have bold style font, + * // system will select the closest font. + * .build(); + * </code> + * </pre> + * + * 3) Create Typeface from single ttf file and if that font does not have glyph for the + * characters, use "serif" font family instead. + * <pre> + * <code> + * Font font = new Font.Builder("your_font_file.ttf").build(); + * FontFamily family = new FontFamily.Builder(font).build(); + * Typeface typeface = new Typeface.CustomFallbackBuilder(family) + * .setSystemFallback("serif") // Set serif font family as the fallback. + * .build(); + * </code> + * </pre> + * 4) Create Typeface from single ttf file and set another ttf file for the fallback. + * <pre> + * <code> + * Font font = new Font.Builder("English.ttf").build(); + * FontFamily family = new FontFamily.Builder(font).build(); + * + * Font fallbackFont = new Font.Builder("Arabic.ttf").build(); + * FontFamily fallbackFamily = new FontFamily.Builder(fallbackFont).build(); + * Typeface typeface = new Typeface.CustomFallbackBuilder(family) + * .addCustomFallback(fallbackFamily) // Specify fallback family. + * .setSystemFallback("serif") // Set serif font family as the fallback. + * .build(); + * </code> + * </pre> + * </p> + */ + public static class CustomFallbackBuilder { + private static final int MAX_CUSTOM_FALLBACK = 64; + private final ArrayList<FontFamily> mFamilies = new ArrayList<>(); + private String mFallbackName = null; + private @Nullable FontStyle mStyle; + + /** + * Constructs a builder with a font family. + * + * @param family a family object + */ + public CustomFallbackBuilder(@NonNull FontFamily family) { + Preconditions.checkNotNull(family); + mFamilies.add(family); + } + + /** + * Sets a system fallback by name. + * + * You can specify generic font familiy names or OEM specific family names. If the system + * don't have a specified fallback, the default fallback is used instead. + * For more information about generic font families, see <a + * href="https://www.w3.org/TR/css-fonts-4/#generic-font-families">CSS specification</a> + * + * For more information about fallback, see class description. + * + * @param familyName a family name to be used for fallback if the provided fonts can not be + * used + */ + public CustomFallbackBuilder setSystemFallback(@NonNull String familyName) { + Preconditions.checkNotNull(familyName); + mFallbackName = familyName; + return this; + } + + /** + * Sets a font style of the Typeface. + * + * If the font family doesn't have a font of given style, system will select the closest + * font from font family. For example, if a font family has fonts of 300 weight and 700 + * weight then setWeight(400) is called, system will select the font of 300 weight. + * + * @param style a font style + */ + public CustomFallbackBuilder setStyle(@NonNull FontStyle style) { + mStyle = style; + return this; + } + + /** + * Append a font family to the end of the custom font fallback. + * + * You can set up to 64 custom fallback families including the first font family you passed + * to the constructor. + * For more information about fallback, see class description. + * + * @param family a fallback family + * @throws IllegalArgumentException if you give more than 64 custom fallback families + */ + public CustomFallbackBuilder addCustomFallback(@NonNull FontFamily family) { + Preconditions.checkNotNull(family); + Preconditions.checkArgument(mFamilies.size() < MAX_CUSTOM_FALLBACK, + "Custom fallback limit exceeded(" + MAX_CUSTOM_FALLBACK + ")"); + mFamilies.add(family); + return this; + } + + /** + * Create the Typeface based on the configured values. + * + * @return the Typeface object + */ + public Typeface build() { + final int userFallbackSize = mFamilies.size(); + final FontFamily[] fallback = SystemFonts.getSystemFallback(mFallbackName); + final FontFamily[] fullFamilies = new FontFamily[fallback.length + userFallbackSize]; + final long[] ptrArray = new long[fallback.length + userFallbackSize]; + for (int i = 0; i < userFallbackSize; ++i) { + ptrArray[i] = mFamilies.get(i).getNativePtr(); + fullFamilies[i] = mFamilies.get(i); + } + for (int i = 0; i < fallback.length; ++i) { + ptrArray[i + userFallbackSize] = fallback[i].getNativePtr(); + fullFamilies[i + userFallbackSize] = fallback[i]; + } + final int weight = mStyle == null ? 400 : mStyle.getWeight(); + final int italic = + (mStyle == null || mStyle.getSlant() == FontStyle.FONT_SLANT_UPRIGHT) ? 0 : 1; + + return new Typeface(nativeCreateFromArray(ptrArray, weight, italic), fullFamilies); } } @@ -687,7 +750,7 @@ public class Typeface { * @return The best matching typeface. */ public static Typeface create(String familyName, @Style int style) { - return create(sSystemFontMap.get(familyName), style); + return create(getSystemDefaultTypeface(familyName), style); } /** @@ -736,7 +799,7 @@ public class Typeface { } } - typeface = new Typeface(nativeCreateFromTypeface(ni, style)); + typeface = new Typeface(nativeCreateFromTypeface(ni, style), family.mFamilies); styles.put(style, typeface); } return typeface; @@ -805,7 +868,7 @@ public class Typeface { typeface = new Typeface( nativeCreateFromTypefaceWithExactStyle( - base.native_instance, weight, italic)); + base.native_instance, weight, italic), base.mFamilies); innerCache.put(key, typeface); } return typeface; @@ -814,8 +877,9 @@ public class 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)); + final Typeface base = family == null ? Typeface.DEFAULT : family; + return new Typeface(nativeCreateFromTypefaceWithVariation(base.native_instance, axes), + base.mFamilies); } /** @@ -897,9 +961,11 @@ public class Typeface { * Create a new typeface from an array of font families. * * @param families array of font families + * @deprecated */ + @Deprecated @UnsupportedAppUsage - private static Typeface createFromFamilies(FontFamily[] families) { + private static Typeface createFromFamilies(android.graphics.FontFamily[] families) { long[] ptrArray = new long[families.length]; for (int i = 0; i < families.length; i++) { ptrArray[i] = families[i].mNativePtr; @@ -909,11 +975,26 @@ public class Typeface { } /** + * Create a new typeface from an array of android.graphics.fonts.FontFamily. + * + * @param families array of font families + */ + private static Typeface createFromFamilies(@Nullable FontFamily[] families) { + final long[] ptrArray = new long[families.length]; + for (int i = 0; i < families.length; ++i) { + ptrArray[i] = families[i].getNativePtr(); + } + return new Typeface(nativeCreateFromArray(ptrArray, + RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE), families); + } + + /** * This method is used by supportlib-v27. * TODO: Remove private API use in supportlib: http://b/72665240 */ - private static Typeface createFromFamiliesWithDefault(FontFamily[] families, int weight, - int italic) { + @UnsupportedAppUsage + private static Typeface createFromFamiliesWithDefault( + android.graphics.FontFamily[] families, int weight, int italic) { return createFromFamiliesWithDefault(families, DEFAULT_FAMILY, weight, italic); } @@ -931,18 +1012,15 @@ public class Typeface { * @param families array of font families */ @UnsupportedAppUsage - private static Typeface createFromFamiliesWithDefault(FontFamily[] families, + private static Typeface createFromFamiliesWithDefault(android.graphics.FontFamily[] families, String fallbackName, int weight, int italic) { - FontFamily[] fallback = sSystemFallbackMap.get(fallbackName); - if (fallback == null) { - fallback = sSystemFallbackMap.get(DEFAULT_FAMILY); - } + android.graphics.fonts.FontFamily[] fallback = SystemFonts.getSystemFallback(fallbackName); 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; + ptrArray[i + families.length] = fallback[i].getNativePtr(); } return new Typeface(nativeCreateFromArray(ptrArray, weight, italic)); } @@ -955,198 +1033,68 @@ public class Typeface { } native_instance = ni; + mFamilies = new FontFamily[0]; sRegistry.registerNativeAllocation(this, native_instance); 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 Typeface(long ni, @NonNull FontFamily[] families) { + if (ni == 0) { + throw new IllegalStateException("native typeface cannot be made"); } - } - 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; + native_instance = ni; + mFamilies = families; + sRegistry.registerNativeAllocation(this, native_instance); + mStyle = nativeGetStyle(ni); + mWeight = nativeGetWeight(ni); } - 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<>(); + private static Typeface getSystemDefaultTypeface(@NonNull String familyName) { + Typeface tf = sSystemFontMap.get(familyName); + return tf == null ? Typeface.DEFAULT : tf; + } - // 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); - } + /** @hide */ + @VisibleForTesting + public static void initSystemDefaultTypefaces(Map<String, Typeface> systemFontMap, + Map<String, FontFamily[]> fallbacks, + FontConfig.Alias[] aliases) { + for (Map.Entry<String, FontFamily[]> entry : fallbacks.entrySet()) { + systemFontMap.put(entry.getKey(), createFromFamilies(entry.getValue())); } - 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); - } else if (defaultFamily != null) { - fallbackMap.valueAt(i).add(defaultFamily); - } else { - // There is no valid for for default fallback. Ignore. - } + for (FontConfig.Alias alias : aliases) { + if (systemFontMap.containsKey(alias.getName())) { + continue; // If alias and named family are conflict, use named family. } + final Typeface base = systemFontMap.get(alias.getToName()); + final int weight = alias.getWeight(); + final Typeface newFace = weight == 400 ? base : + new Typeface(nativeCreateWeightAlias(base.native_instance, weight), + base.mFamilies); + systemFontMap.put(alias.getName(), newFace); } } - /** - * 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); + private static void registerGenericFamilyNative(@NonNull String familyName, + @Nullable Typeface typeface) { + if (typeface != null) { + nativeRegisterGenericFamily(familyName, typeface.native_instance); } } static { - final ArrayMap<String, Typeface> systemFontMap = new ArrayMap<>(); - final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>(); - buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", systemFontMap, - systemFallbackMap); + final HashMap<String, Typeface> systemFontMap = new HashMap<>(); + initSystemDefaultTypefaces(systemFontMap, SystemFonts.getRawSystemFallbackMap(), + SystemFonts.getAliases()); sSystemFontMap = Collections.unmodifiableMap(systemFontMap); - sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap); - setDefault(sSystemFontMap.get(DEFAULT_FAMILY)); + // We can't assume DEFAULT_FAMILY available on Roboletric. + if (sSystemFontMap.containsKey(DEFAULT_FAMILY)) { + setDefault(sSystemFontMap.get(DEFAULT_FAMILY)); + } // Set up defaults and typefaces exposed in public API DEFAULT = create((String) null, 0); @@ -1162,6 +1110,15 @@ public class Typeface { create((String) null, Typeface.BOLD_ITALIC), }; + // A list of generic families to be registered in native. + // https://www.w3.org/TR/css-fonts-4/#generic-font-families + String[] genericFamilies = { + "serif", "sans-serif", "cursive", "fantasy", "monospace", "system-ui" + }; + + for (String genericFamily : genericFamilies) { + registerGenericFamilyNative(genericFamily, systemFontMap.get(genericFamily)); + } } @Override @@ -1224,4 +1181,6 @@ public class Typeface { @CriticalNative private static native long nativeGetReleaseFunc(); + + private static native void nativeRegisterGenericFamily(String str, long nativePtr); } diff --git a/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java b/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java index 4f467d9aabae..3aaec3123d7d 100644 --- a/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java +++ b/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java @@ -19,6 +19,7 @@ package android.graphics.drawable; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.UnsupportedAppUsage; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.content.res.Resources.Theme; @@ -562,6 +563,7 @@ public class AnimatedImageDrawable extends Drawable implements Animatable2 { * callback, so no need to post. */ @SuppressWarnings("unused") + @UnsupportedAppUsage private void onAnimationEnd() { if (mAnimationCallbacks != null) { for (Animatable2.AnimationCallback callback : mAnimationCallbacks) { diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java index 19ca098d57db..789e38c4e650 100644 --- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java +++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java @@ -39,7 +39,9 @@ import android.graphics.Insets; import android.graphics.Outline; import android.graphics.PixelFormat; import android.graphics.PorterDuff; +import android.graphics.RecordingCanvas; import android.graphics.Rect; +import android.graphics.RenderNode; import android.os.Build; import android.util.ArrayMap; import android.util.AttributeSet; @@ -50,8 +52,7 @@ 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.NativeVectorDrawableAnimator; import android.view.RenderNodeAnimatorSetHelper; import android.view.View; @@ -519,7 +520,6 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { mAnimatedVectorState.mVectorDrawable.getOutline(outline); } - /** @hide */ @Override public Insets getOpticalInsets() { return mAnimatedVectorState.mVectorDrawable.getOpticalInsets(); @@ -1232,7 +1232,8 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { /** * @hide */ - public static class VectorDrawableAnimatorRT implements VectorDrawableAnimator { + public static class VectorDrawableAnimatorRT implements VectorDrawableAnimator, + NativeVectorDrawableAnimator { private static final int START_ANIMATION = 1; private static final int REVERSE_ANIMATION = 2; private static final int RESET_ANIMATION = 3; @@ -1541,11 +1542,11 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { } /** - * Holds a weak reference to the target that was last seen (through the DisplayListCanvas + * Holds a weak reference to the target that was last seen (through the RecordingCanvas * 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) { + protected void recordLastSeenTarget(RecordingCanvas canvas) { final RenderNode node = RenderNodeAnimatorSetHelper.getTarget(canvas); mLastSeenTarget = new WeakReference<RenderNode>(node); // Add the animator to the list of animators on every draw @@ -1705,6 +1706,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { } } + @Override public long getAnimatorNativePtr() { return mSetPtr; } @@ -1740,7 +1742,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { @Override public void onDraw(Canvas canvas) { if (canvas.isHardwareAccelerated()) { - recordLastSeenTarget((DisplayListCanvas) canvas); + recordLastSeenTarget((RecordingCanvas) canvas); } } diff --git a/graphics/java/android/graphics/drawable/BitmapDrawable.java b/graphics/java/android/graphics/drawable/BitmapDrawable.java index 9d9b3eb159c8..976190109e7c 100644 --- a/graphics/java/android/graphics/drawable/BitmapDrawable.java +++ b/graphics/java/android/graphics/drawable/BitmapDrawable.java @@ -24,7 +24,6 @@ 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; @@ -631,9 +630,6 @@ public class BitmapDrawable extends Drawable { mDstRectAndInsetsDirty = false; } - /** - * @hide - */ @Override public Insets getOpticalInsets() { updateDstRectAndInsetsIfDirty(); diff --git a/graphics/java/android/graphics/drawable/ColorDrawable.java b/graphics/java/android/graphics/drawable/ColorDrawable.java index 7cbe92910f57..3c449166d9d5 100644 --- a/graphics/java/android/graphics/drawable/ColorDrawable.java +++ b/graphics/java/android/graphics/drawable/ColorDrawable.java @@ -22,12 +22,18 @@ import android.annotation.Nullable; import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; 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.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Xfermode; import android.util.AttributeSet; import android.view.ViewDebug; @@ -143,9 +149,13 @@ public class ColorDrawable extends Drawable { } /** - * Returns the alpha value of this drawable's color. + * Returns the alpha value of this drawable's color. Note this may not be the same alpha value + * provided in {@link Drawable#setAlpha(int)}. Instead this will return the alpha of the color + * combined with the alpha provided by setAlpha * * @return A value between 0 and 255. + * + * @see ColorDrawable#setAlpha(int) */ @Override public int getAlpha() { @@ -153,7 +163,9 @@ public class ColorDrawable extends Drawable { } /** - * Sets the color's alpha value. + * Applies the given alpha to the underlying color. Note if the color already has + * an alpha applied to it, this will apply this alpha to the existing value instead of + * overwriting it. * * @param alpha The alpha value to set, between 0 and 255. */ @@ -182,6 +194,17 @@ public class ColorDrawable extends Drawable { mPaint.setColorFilter(colorFilter); } + /** + * Returns the color filter applied to this color configured by + * {@link #setColorFilter(ColorFilter)} + * + * @see android.graphics.drawable.Drawable#getColorFilter() + */ + @Override + public @Nullable ColorFilter getColorFilter() { + return mPaint.getColorFilter(); + } + @Override public void setTintList(ColorStateList tint) { mColorState.mTint = tint; diff --git a/graphics/java/android/graphics/drawable/ColorStateListDrawable.java b/graphics/java/android/graphics/drawable/ColorStateListDrawable.java new file mode 100644 index 000000000000..c0c6a4f424a1 --- /dev/null +++ b/graphics/java/android/graphics/drawable/ColorStateListDrawable.java @@ -0,0 +1,304 @@ +/* + * Copyright 2018 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.IntRange; +import android.annotation.NonNull; +import android.content.pm.ActivityInfo; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.util.MathUtils; + +/** + * A Drawable that manages a {@link ColorDrawable} to make it stateful and backed by a + * {@link ColorStateList}. + */ +public class ColorStateListDrawable extends Drawable implements Drawable.Callback { + private ColorDrawable mColorDrawable; + private ColorStateListDrawableState mState; + private boolean mMutated = false; + + public ColorStateListDrawable() { + mState = new ColorStateListDrawableState(); + initializeColorDrawable(); + } + + public ColorStateListDrawable(ColorStateList colorStateList) { + mState = new ColorStateListDrawableState(); + initializeColorDrawable(); + setColorStateList(colorStateList); + } + + @Override + public void draw(Canvas canvas) { + mColorDrawable.draw(canvas); + } + + @Override + @IntRange(from = 0, to = 255) + public int getAlpha() { + return mColorDrawable.getAlpha(); + } + + @Override + public boolean isStateful() { + return mState.isStateful(); + } + + @Override + public boolean hasFocusStateSpecified() { + return mState.hasFocusStateSpecified(); + } + + @Override + public @NonNull Drawable getCurrent() { + return mColorDrawable; + } + + @Override + public void applyTheme(Resources.Theme t) { + super.applyTheme(t); + + if (mState.mColor != null) { + setColorStateList(mState.mColor.obtainForTheme(t)); + } + + if (mState.mTint != null) { + setTintList(mState.mTint.obtainForTheme(t)); + } + } + + @Override + public boolean canApplyTheme() { + return super.canApplyTheme() || mState.canApplyTheme(); + } + + @Override + public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { + mState.mAlpha = alpha; + onStateChange(getState()); + } + + /** + * Remove the alpha override, reverting to the alpha defined on each color in the + * {@link ColorStateList}. + */ + public void clearAlpha() { + mState.mAlpha = -1; + onStateChange(getState()); + } + + @Override + public void setTintList(ColorStateList tint) { + mState.mTint = tint; + mColorDrawable.setTintList(tint); + onStateChange(getState()); + } + + @Override + public void setTintMode(PorterDuff.Mode tintMode) { + mState.mTintMode = tintMode; + mColorDrawable.setTintMode(tintMode); + onStateChange(getState()); + } + + @Override + public ColorFilter getColorFilter() { + return mColorDrawable.getColorFilter(); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mColorDrawable.setColorFilter(colorFilter); + } + + @Override + public @PixelFormat.Opacity int getOpacity() { + return mColorDrawable.getOpacity(); + } + + @Override + protected boolean onStateChange(int[] state) { + if (mState.mColor != null) { + int color = mState.mColor.getColorForState(state, mState.mColor.getDefaultColor()); + + if (mState.mAlpha != -1) { + color = (color & 0xFFFFFF) | MathUtils.constrain(mState.mAlpha, 0, 255) << 24; + } + + if (color != mColorDrawable.getColor()) { + mColorDrawable.setColor(color); + mColorDrawable.setState(state); + return true; + } else { + return mColorDrawable.setState(state); + } + } else { + return false; + } + } + + @Override + public void invalidateDrawable(Drawable who) { + final Callback callback = getCallback(); + + if (callback != null) { + callback.invalidateDrawable(who); + } + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + final Callback callback = getCallback(); + + if (callback != null) { + callback.scheduleDrawable(who, what, when); + } + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + final Callback callback = getCallback(); + + if (callback != null) { + callback.unscheduleDrawable(who, what); + } + } + + @Override + public ConstantState getConstantState() { + mState.mChangingConfigurations = mState.mChangingConfigurations + | (getChangingConfigurations() & ~mState.getChangingConfigurations()); + return mState; + } + + /** + * Returns the ColorStateList backing this Drawable, or a new ColorStateList of the default + * ColorDrawable color if one hasn't been defined yet. + * + * @return a ColorStateList + */ + public @NonNull ColorStateList getColorStateList() { + if (mState.mColor == null) { + return ColorStateList.valueOf(mColorDrawable.getColor()); + } else { + return mState.mColor; + } + } + + @Override + public int getChangingConfigurations() { + return super.getChangingConfigurations() | mState.getChangingConfigurations(); + } + + @Override + public Drawable mutate() { + if (!mMutated && super.mutate() == this) { + mState = new ColorStateListDrawableState(mState); + mMutated = true; + } + return this; + } + + /** + * @hide + */ + @Override + public void clearMutated() { + super.clearMutated(); + mMutated = false; + } + + /** + * Replace this Drawable's ColorStateList. It is not copied, so changes will propagate on the + * next call to {@link #setState(int[])}. + * + * @param colorStateList A color state list to attach. + */ + public void setColorStateList(ColorStateList colorStateList) { + mState.mColor = colorStateList; + onStateChange(getState()); + } + + static final class ColorStateListDrawableState extends ConstantState { + ColorStateList mColor = null; + ColorStateList mTint = null; + int mAlpha = -1; + PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE; + @ActivityInfo.Config int mChangingConfigurations = 0; + + ColorStateListDrawableState() { + } + + ColorStateListDrawableState(ColorStateListDrawableState state) { + mColor = state.mColor; + mTint = state.mTint; + mAlpha = state.mAlpha; + mTintMode = state.mTintMode; + mChangingConfigurations = state.mChangingConfigurations; + } + + @Override + public Drawable newDrawable() { + return new ColorStateListDrawable(this); + } + + @Override + public @ActivityInfo.Config int getChangingConfigurations() { + return mChangingConfigurations + | (mColor != null ? mColor.getChangingConfigurations() : 0) + | (mTint != null ? mTint.getChangingConfigurations() : 0); + } + + public boolean isStateful() { + return (mColor != null && mColor.isStateful()) + || (mTint != null && mTint.isStateful()); + } + + public boolean hasFocusStateSpecified() { + return (mColor != null && mColor.hasFocusStateSpecified()) + || (mTint != null && mTint.hasFocusStateSpecified()); + } + + @Override + public boolean canApplyTheme() { + return (mColor != null && mColor.canApplyTheme()) + || (mTint != null && mTint.canApplyTheme()); + } + } + + private ColorStateListDrawable(ColorStateListDrawableState state) { + mState = state; + initializeColorDrawable(); + } + + private void initializeColorDrawable() { + mColorDrawable = new ColorDrawable(); + mColorDrawable.setCallback(this); + + if (mState.mTint != null) { + mColorDrawable.setTintList(mState.mTint); + } + + if (mState.mTintMode != DEFAULT_TINT_MODE) { + mColorDrawable.setTintMode(mState.mTintMode); + } + } +} diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java index 2a5254191527..caf610b8c236 100644 --- a/graphics/java/android/graphics/drawable/Drawable.java +++ b/graphics/java/android/graphics/drawable/Drawable.java @@ -16,11 +16,6 @@ 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; @@ -58,6 +53,11 @@ import android.util.TypedValue; import android.util.Xml; import android.view.View; +import com.android.internal.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -934,11 +934,13 @@ public abstract class Drawable { * do account for the value of {@link #setAlpha}, but the general behavior is dependent * upon the implementation of the subclass. * + * @deprecated This method is no longer used in graphics optimizations + * * @return int The opacity class of the Drawable. * * @see android.graphics.PixelFormat */ - public abstract @PixelFormat.Opacity int getOpacity(); + @Deprecated public abstract @PixelFormat.Opacity int getOpacity(); /** * Return the appropriate opacity value for two source opacities. If @@ -1092,7 +1094,6 @@ public abstract class Drawable { * 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; @@ -1522,12 +1523,11 @@ public abstract class Drawable { } final int color = tint.getColorForState(getState(), Color.TRANSPARENT); - if (tintFilter == null) { + if (tintFilter == null || tintFilter.getColor() != color + || tintFilter.getMode() != tintMode) { return new PorterDuffColorFilter(color, tintMode); } - tintFilter.setColor(color); - tintFilter.setMode(tintMode); return tintFilter; } diff --git a/graphics/java/android/graphics/drawable/DrawableContainer.java b/graphics/java/android/graphics/drawable/DrawableContainer.java index 8c893e0d88f3..10f6fae3cf6d 100644 --- a/graphics/java/android/graphics/drawable/DrawableContainer.java +++ b/graphics/java/android/graphics/drawable/DrawableContainer.java @@ -123,9 +123,6 @@ public class DrawableContainer extends Drawable implements Drawable.Callback { return result; } - /** - * @hide - */ @Override public Insets getOpticalInsets() { if (mCurrDrawable != null) { diff --git a/graphics/java/android/graphics/drawable/DrawableWrapper.java b/graphics/java/android/graphics/drawable/DrawableWrapper.java index a12575af939f..4ee45bfd8bc8 100644 --- a/graphics/java/android/graphics/drawable/DrawableWrapper.java +++ b/graphics/java/android/graphics/drawable/DrawableWrapper.java @@ -16,11 +16,6 @@ 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.annotation.UnsupportedAppUsage; @@ -36,10 +31,16 @@ import android.graphics.Outline; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.Rect; +import android.graphics.Xfermode; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.View; +import com.android.internal.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; /** @@ -80,6 +81,16 @@ public abstract class DrawableWrapper extends Drawable implements Drawable.Callb } /** + * @hide + */ + @Override + public void setXfermode(Xfermode mode) { + if (mDrawable != null) { + mDrawable.setXfermode(mode); + } + } + + /** * Sets the wrapped drawable. * * @param dr the wrapped drawable @@ -242,7 +253,6 @@ public abstract class DrawableWrapper extends Drawable implements Drawable.Callb return mDrawable != null && mDrawable.getPadding(padding); } - /** @hide */ @Override public Insets getOpticalInsets() { return mDrawable != null ? mDrawable.getOpticalInsets() : Insets.NONE; diff --git a/graphics/java/android/graphics/drawable/GradientDrawable.java b/graphics/java/android/graphics/drawable/GradientDrawable.java index d34d461e8a87..87402342a3c9 100644 --- a/graphics/java/android/graphics/drawable/GradientDrawable.java +++ b/graphics/java/android/graphics/drawable/GradientDrawable.java @@ -397,6 +397,7 @@ public class GradientDrawable extends Drawable { e = new DashPathEffect(new float[] { dashWidth, dashGap }, 0); } mStrokePaint.setPathEffect(e); + mGradientIsDirty = true; invalidateSelf(); } @@ -935,16 +936,15 @@ public class GradientDrawable extends Drawable { * @see #getColor */ public void setColor(@Nullable ColorStateList colorStateList) { - mGradientState.setSolidColors(colorStateList); - final int color; if (colorStateList == null) { - color = Color.TRANSPARENT; + setColor(Color.TRANSPARENT); } else { final int[] stateSet = getState(); - color = colorStateList.getColorForState(stateSet, 0); + final int color = colorStateList.getColorForState(stateSet, 0); + mGradientState.setSolidColors(colorStateList); + mFillPaint.setColor(color); + invalidateSelf(); } - mFillPaint.setColor(color); - invalidateSelf(); } /** @@ -1703,7 +1703,6 @@ public class GradientDrawable extends Drawable { return mGradientState.mHeight; } - /** @hide */ @Override public Insets getOpticalInsets() { return mGradientState.mOpticalInsets; diff --git a/graphics/java/android/graphics/drawable/InsetDrawable.java b/graphics/java/android/graphics/drawable/InsetDrawable.java index f342dde24d01..bc8a4cbd7e9d 100644 --- a/graphics/java/android/graphics/drawable/InsetDrawable.java +++ b/graphics/java/android/graphics/drawable/InsetDrawable.java @@ -16,11 +16,6 @@ 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.annotation.UnsupportedAppUsage; @@ -36,6 +31,11 @@ import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.TypedValue; +import com.android.internal.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; /** @@ -242,7 +242,6 @@ public class InsetDrawable extends DrawableWrapper { | mTmpInsetRect.top | mTmpInsetRect.bottom) != 0; } - /** @hide */ @Override public Insets getOpticalInsets() { final Insets contentInsets = super.getOpticalInsets(); diff --git a/graphics/java/android/graphics/drawable/NinePatchDrawable.java b/graphics/java/android/graphics/drawable/NinePatchDrawable.java index 68dd9137ae7a..b53477137331 100644 --- a/graphics/java/android/graphics/drawable/NinePatchDrawable.java +++ b/graphics/java/android/graphics/drawable/NinePatchDrawable.java @@ -301,9 +301,6 @@ public class NinePatchDrawable extends Drawable { super.getOutline(outline); } - /** - * @hide - */ @Override public Insets getOpticalInsets() { final Insets opticalInsets = mOpticalInsets; diff --git a/graphics/java/android/graphics/drawable/RippleComponent.java b/graphics/java/android/graphics/drawable/RippleComponent.java index 626bcee9454b..c1f8798faaeb 100644 --- a/graphics/java/android/graphics/drawable/RippleComponent.java +++ b/graphics/java/android/graphics/drawable/RippleComponent.java @@ -16,15 +16,8 @@ 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 size & positioning common to the ripple & focus states. diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java index 026dbbbbdfd2..1540cc22e295 100644 --- a/graphics/java/android/graphics/drawable/RippleDrawable.java +++ b/graphics/java/android/graphics/drawable/RippleDrawable.java @@ -892,7 +892,10 @@ public class RippleDrawable extends LayerDrawable { // 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. - mMaskColorFilter.setColor(color | 0xFF000000); + int maskColor = color | 0xFF000000; + if (mMaskColorFilter.getColor() != maskColor) { + mMaskColorFilter = new PorterDuffColorFilter(maskColor, mMaskColorFilter.getMode()); + } p.setColor(color & 0xFF000000); p.setColorFilter(mMaskColorFilter); p.setShader(mMaskShader); diff --git a/graphics/java/android/graphics/drawable/RippleForeground.java b/graphics/java/android/graphics/drawable/RippleForeground.java index a8dc34af292b..cce9ba31929f 100644 --- a/graphics/java/android/graphics/drawable/RippleForeground.java +++ b/graphics/java/android/graphics/drawable/RippleForeground.java @@ -23,10 +23,10 @@ import android.animation.TimeInterpolator; import android.graphics.Canvas; import android.graphics.CanvasProperty; import android.graphics.Paint; +import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.util.FloatProperty; import android.util.MathUtils; -import android.view.DisplayListCanvas; import android.view.RenderNodeAnimator; import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; @@ -132,7 +132,7 @@ class RippleForeground extends RippleComponent { } } - private void startPending(DisplayListCanvas c) { + private void startPending(RecordingCanvas c) { if (!mPendingHwAnimators.isEmpty()) { for (int i = 0; i < mPendingHwAnimators.size(); i++) { RenderNodeAnimator animator = mPendingHwAnimators.get(i); @@ -164,7 +164,7 @@ class RippleForeground extends RippleComponent { } } - private void drawHardware(DisplayListCanvas c, Paint p) { + private void drawHardware(RecordingCanvas c, Paint p) { startPending(c); pruneHwFinished(); if (mPropPaint != null) { @@ -332,11 +332,11 @@ class RippleForeground extends RippleComponent { * @param p the paint used to draw the ripple */ public void draw(Canvas c, Paint p) { - final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof DisplayListCanvas; + final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof RecordingCanvas; pruneSwFinished(); if (hasDisplayListCanvas) { - final DisplayListCanvas hw = (DisplayListCanvas) c; + final RecordingCanvas hw = (RecordingCanvas) c; drawHardware(hw, p); } else { drawSoftware(c, p); diff --git a/graphics/java/android/graphics/drawable/ShapeDrawable.java b/graphics/java/android/graphics/drawable/ShapeDrawable.java index 34da928bb6f1..7bfb4c38d6c8 100644 --- a/graphics/java/android/graphics/drawable/ShapeDrawable.java +++ b/graphics/java/android/graphics/drawable/ShapeDrawable.java @@ -594,12 +594,12 @@ public class ShapeDrawable extends Drawable { @Override public Drawable newDrawable() { - return new ShapeDrawable(this, null); + return new ShapeDrawable(new ShapeState(this), null); } @Override public Drawable newDrawable(Resources res) { - return new ShapeDrawable(this, res); + return new ShapeDrawable(new ShapeState(this), res); } @Override diff --git a/graphics/java/android/graphics/drawable/StateListDrawable.java b/graphics/java/android/graphics/drawable/StateListDrawable.java index fa18f64981b9..2855227a1002 100644 --- a/graphics/java/android/graphics/drawable/StateListDrawable.java +++ b/graphics/java/android/graphics/drawable/StateListDrawable.java @@ -16,23 +16,23 @@ 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.annotation.UnsupportedAppUsage; import android.content.res.Resources; -import android.content.res.TypedArray; import android.content.res.Resources.Theme; +import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.StateSet; +import com.android.internal.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.Arrays; + /** * Lets you assign a number of graphic images to a single Drawable and swap out the visible item by a string * ID value. @@ -75,9 +75,11 @@ public class StateListDrawable extends DrawableContainer { /** * Add a new image/string ID to the set of images. * - * @param stateSet - An array of resource Ids to associate with the image. + * @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. + * @param drawable The image to show. Note this must be a unique Drawable that is not shared + * between any other View or Drawable otherwise the results are + * undefined and can lead to unexpected rendering behavior */ public void addState(int[] stateSet, Drawable drawable) { if (drawable != null) { @@ -239,7 +241,6 @@ public class StateListDrawable extends DrawableContainer { * 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) */ @@ -252,7 +253,6 @@ public class StateListDrawable extends DrawableContainer { * * @param index The index of the state set. * @return The state set at the index. - * @hide pending API council * @see #getStateCount() * @see #getStateDrawable(int) */ @@ -265,7 +265,6 @@ public class StateListDrawable extends DrawableContainer { * * @param index The index of the drawable. * @return The drawable at the index. - * @hide pending API council * @see #getStateCount() * @see #getStateSet(int) */ @@ -278,11 +277,10 @@ public class StateListDrawable extends DrawableContainer { * * @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) { + public int findStateDrawableIndex(int[] stateSet) { return mStateListState.indexOfStateSet(stateSet); } diff --git a/graphics/java/android/graphics/drawable/VectorDrawable.java b/graphics/java/android/graphics/drawable/VectorDrawable.java index 09badff01681..7325b857f02c 100644 --- a/graphics/java/android/graphics/drawable/VectorDrawable.java +++ b/graphics/java/android/graphics/drawable/VectorDrawable.java @@ -47,6 +47,9 @@ import android.util.Xml; import com.android.internal.R; import com.android.internal.util.VirtualRefBasePtr; +import dalvik.annotation.optimization.FastNative; +import dalvik.system.VMRuntime; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -57,9 +60,6 @@ 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/> @@ -546,7 +546,6 @@ public class VectorDrawable extends Drawable { return mDpiScaledHeight; } - /** @hide */ @Override public Insets getOpticalInsets() { if (mDpiScaledDirty) { @@ -2034,7 +2033,7 @@ public class VectorDrawable extends Drawable { if (fillColors instanceof GradientColor) { mFillColors = fillColors; fillGradient = ((GradientColor) fillColors).getShader(); - } else if (fillColors.isStateful()) { + } else if (fillColors.isStateful() || fillColors.canApplyTheme()) { mFillColors = fillColors; } else { mFillColors = null; @@ -2050,7 +2049,7 @@ public class VectorDrawable extends Drawable { if (strokeColors instanceof GradientColor) { mStrokeColors = strokeColors; strokeGradient = ((GradientColor) strokeColors).getShader(); - } else if (strokeColors.isStateful()) { + } else if (strokeColors.isStateful() || strokeColors.canApplyTheme()) { mStrokeColors = strokeColors; } else { mStrokeColors = null; diff --git a/graphics/java/android/graphics/drawable/shapes/ArcShape.java b/graphics/java/android/graphics/drawable/shapes/ArcShape.java index 85ba0a9e9f6f..90d07bce2617 100644 --- a/graphics/java/android/graphics/drawable/shapes/ArcShape.java +++ b/graphics/java/android/graphics/drawable/shapes/ArcShape.java @@ -20,6 +20,8 @@ import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Paint; +import java.util.Objects; + /** * Creates an arc shape. The arc shape starts at a specified angle and sweeps * clockwise, drawing slices of pie. @@ -74,5 +76,26 @@ public class ArcShape extends RectShape { public ArcShape clone() throws CloneNotSupportedException { return (ArcShape) super.clone(); } + + @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; + } + ArcShape arcShape = (ArcShape) o; + return Float.compare(arcShape.mStartAngle, mStartAngle) == 0 + && Float.compare(arcShape.mSweepAngle, mSweepAngle) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), mStartAngle, mSweepAngle); + } } diff --git a/graphics/java/android/graphics/drawable/shapes/PathShape.java b/graphics/java/android/graphics/drawable/shapes/PathShape.java index ce5552b7a1bb..393fdee87bb8 100644 --- a/graphics/java/android/graphics/drawable/shapes/PathShape.java +++ b/graphics/java/android/graphics/drawable/shapes/PathShape.java @@ -21,6 +21,8 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; +import java.util.Objects; + /** * Creates geometric paths, utilizing the {@link android.graphics.Path} class. * <p> @@ -74,5 +76,30 @@ public class PathShape extends Shape { shape.mPath = new Path(mPath); return shape; } + + @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; + } + PathShape pathShape = (PathShape) o; + return Float.compare(pathShape.mStdWidth, mStdWidth) == 0 + && Float.compare(pathShape.mStdHeight, mStdHeight) == 0 + && Float.compare(pathShape.mScaleX, mScaleX) == 0 + && Float.compare(pathShape.mScaleY, mScaleY) == 0 + // Path does not have equals implementation but incase it gains one, use it here + && Objects.equals(mPath, pathShape.mPath); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), mStdWidth, mStdHeight, mPath, mScaleX, mScaleY); + } } diff --git a/graphics/java/android/graphics/drawable/shapes/RectShape.java b/graphics/java/android/graphics/drawable/shapes/RectShape.java index e339a212a794..1bbd1d7236d9 100644 --- a/graphics/java/android/graphics/drawable/shapes/RectShape.java +++ b/graphics/java/android/graphics/drawable/shapes/RectShape.java @@ -21,6 +21,8 @@ import android.graphics.Outline; import android.graphics.Paint; import android.graphics.RectF; +import java.util.Objects; + /** * Defines a rectangle shape. * <p> @@ -63,4 +65,24 @@ public class RectShape extends Shape { shape.mRect = new RectF(mRect); return shape; } + + @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; + } + RectShape rectShape = (RectShape) o; + return Objects.equals(mRect, rectShape.mRect); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), mRect); + } } diff --git a/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java b/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java index f5cbb24a53ef..475e0bb70f2b 100644 --- a/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java +++ b/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java @@ -23,6 +23,9 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; +import java.util.Arrays; +import java.util.Objects; + /** * Creates a rounded-corner rectangle. Optionally, an inset (rounded) rectangle * can be included (to make a sort of "O" shape). @@ -137,4 +140,31 @@ public class RoundRectShape extends RectShape { shape.mPath = new Path(mPath); return shape; } + + @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; + } + RoundRectShape that = (RoundRectShape) o; + return Arrays.equals(mOuterRadii, that.mOuterRadii) + && Objects.equals(mInset, that.mInset) + && Arrays.equals(mInnerRadii, that.mInnerRadii) + && Objects.equals(mInnerRect, that.mInnerRect) + && Objects.equals(mPath, that.mPath); + } + + @Override + public int hashCode() { + int result = Objects.hash(super.hashCode(), mInset, mInnerRect, mPath); + result = 31 * result + Arrays.hashCode(mOuterRadii); + result = 31 * result + Arrays.hashCode(mInnerRadii); + return result; + } } diff --git a/graphics/java/android/graphics/drawable/shapes/Shape.java b/graphics/java/android/graphics/drawable/shapes/Shape.java index 30b28f3c2242..3b044ec631a2 100644 --- a/graphics/java/android/graphics/drawable/shapes/Shape.java +++ b/graphics/java/android/graphics/drawable/shapes/Shape.java @@ -21,6 +21,8 @@ import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Paint; +import java.util.Objects; + /** * Defines a generic graphical "shape." * <p> @@ -115,4 +117,22 @@ public abstract class Shape implements Cloneable { public Shape clone() throws CloneNotSupportedException { return (Shape) super.clone(); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Shape shape = (Shape) o; + return Float.compare(shape.mWidth, mWidth) == 0 + && Float.compare(shape.mHeight, mHeight) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(mWidth, mHeight); + } } diff --git a/graphics/java/android/graphics/fonts/Font.java b/graphics/java/android/graphics/fonts/Font.java new file mode 100644 index 000000000000..7f165bfc6a7d --- /dev/null +++ b/graphics/java/android/graphics/fonts/Font.java @@ -0,0 +1,541 @@ +/* + * Copyright (C) 2018 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.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.os.LocaleList; +import android.util.TypedValue; + +import com.android.internal.util.Preconditions; + +import dalvik.annotation.optimization.CriticalNative; + +import libcore.util.NativeAllocationRegistry; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Arrays; +import java.util.Objects; + +/** + * A font class can be used for creating FontFamily. + */ +public final class Font { + private static final String TAG = "Font"; + + private static final int NOT_SPECIFIED = -1; + private static final int STYLE_ITALIC = 1; + private static final int STYLE_NORMAL = 0; + + /** + * A builder class for creating new Font. + */ + public static class Builder { + private static final NativeAllocationRegistry sAssetByteBufferRegistroy = + new NativeAllocationRegistry(ByteBuffer.class.getClassLoader(), + nGetReleaseNativeAssetFunc(), 64); + + private static final NativeAllocationRegistry sFontRegistory = + new NativeAllocationRegistry(Font.class.getClassLoader(), + nGetReleaseNativeFont(), 64); + + private @Nullable ByteBuffer mBuffer; + private @Nullable File mFile; + private @NonNull String mLocaleList = ""; + private @IntRange(from = -1, to = 1000) int mWeight = NOT_SPECIFIED; + private @IntRange(from = -1, to = 1) int mItalic = NOT_SPECIFIED; + private @IntRange(from = 0) int mTtcIndex = 0; + private @Nullable FontVariationAxis[] mAxes = null; + private @Nullable IOException mException; + + /** + * Constructs a builder with a byte buffer. + * + * Note that only direct buffer can be used as the source of font data. + * + * @see ByteBuffer#allocateDirect(int) + * @param buffer a byte buffer of a font data + */ + public Builder(@NonNull ByteBuffer buffer) { + Preconditions.checkNotNull(buffer, "buffer can not be null"); + if (!buffer.isDirect()) { + throw new IllegalArgumentException( + "Only direct buffer can be used as the source of font data."); + } + mBuffer = buffer; + } + + /** + * Construct a builder with a byte buffer and file path. + * + * This method is intended to be called only from SystemFonts. + * @hide + */ + public Builder(@NonNull ByteBuffer buffer, @NonNull File path, + @NonNull String localeList) { + this(buffer); + mFile = path; + mLocaleList = localeList; + } + + /** + * Constructs a builder with a file path. + * + * @param path a file path to the font file + */ + public Builder(@NonNull File path) { + Preconditions.checkNotNull(path, "path can not be null"); + try (FileInputStream fis = new FileInputStream(path)) { + final FileChannel fc = fis.getChannel(); + mBuffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); + } catch (IOException e) { + mException = e; + } + mFile = path; + } + + /** + * Constructs a builder with a file descriptor. + * + * @param fd a file descriptor + */ + public Builder(@NonNull FileDescriptor fd) { + this(fd, 0, -1); + } + + /** + * Constructs a builder with a file descriptor. + * + * @param fd a file descriptor + * @param offset an offset to of the font data in the file + * @param size a size of the font data. If -1 is passed, use until end of the file. + */ + public Builder(@NonNull FileDescriptor fd, @IntRange(from = 0) long offset, + @IntRange(from = -1) long size) { + try (FileInputStream fis = new FileInputStream(fd)) { + final FileChannel fc = fis.getChannel(); + size = (size == -1) ? fc.size() - offset : size; + mBuffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, size); + } catch (IOException e) { + mException = e; + } + } + + /** + * Constructs a builder from an asset manager and a file path in an asset directory. + * + * @param am the application's asset manager + * @param path the file name of the font data in the asset directory + */ + public Builder(@NonNull AssetManager am, @NonNull String path) { + this(am, path, true /* is asset */, 0 /* cookie */); + } + + /** + * Constructs a builder from an asset manager and a file path in an asset directory. + * + * @param am the application's asset manager + * @param path the file name of the font data in the asset directory + * @param isAsset true if the undelying data is in asset + * @param cookie set asset cookie + * @hide + */ + public Builder(@NonNull AssetManager am, @NonNull String path, boolean isAsset, + int cookie) { + final long nativeAsset = nGetNativeAsset(am, path, isAsset, cookie); + if (nativeAsset == 0) { + mException = new FileNotFoundException("Unable to open " + path); + return; + } + final ByteBuffer b = nGetAssetBuffer(nativeAsset); + sAssetByteBufferRegistroy.registerNativeAllocation(b, nativeAsset); + if (b == null) { + mException = new FileNotFoundException(path + " not found"); + return; + } + mBuffer = b; + } + + /** + * Constructs a builder from resources. + * + * Resource ID must points the font file. XML font can not be used here. + * + * @param res the resource of this application. + * @param resId the resource ID of font file. + */ + public Builder(@NonNull Resources res, int resId) { + final TypedValue value = new TypedValue(); + res.getValue(resId, value, true); + if (value.string == null) { + mException = new FileNotFoundException(resId + " not found"); + return; + } + final String str = value.string.toString(); + if (str.toLowerCase().endsWith(".xml")) { + mException = new FileNotFoundException(resId + " must be font file."); + return; + } + final long nativeAsset = nGetNativeAsset(res.getAssets(), str, false /* is asset */, + value.assetCookie); + if (nativeAsset == 0) { + mException = new FileNotFoundException("Unable to open " + str); + return; + } + final ByteBuffer b = nGetAssetBuffer(nativeAsset); + sAssetByteBufferRegistroy.registerNativeAllocation(b, nativeAsset); + if (b == null) { + mException = new FileNotFoundException(str + " not found"); + return; + } + mBuffer = b; + } + + /** + * Sets weight of the font. + * + * Tells the system the weight of the given font. If this function is not called, the system + * will resolve the weight value by reading font tables. + * + * Here are pairs of the common names and their values. + * <p> + * <table> + * <thead> + * <tr> + * <th align="center">Value</th> + * <th align="center">Name</th> + * <th align="center">Android Definition</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">100</td> + * <td align="center">Thin</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_THIN}</td> + * </tr> + * <tr> + * <td align="center">200</td> + * <td align="center">Extra Light (Ultra Light)</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_EXTRA_LIGHT}</td> + * </tr> + * <tr> + * <td align="center">300</td> + * <td align="center">Light</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_LIGHT}</td> + * </tr> + * <tr> + * <td align="center">400</td> + * <td align="center">Normal (Regular)</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_NORMAL}</td> + * </tr> + * <tr> + * <td align="center">500</td> + * <td align="center">Medium</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_MEDIUM}</td> + * </tr> + * <tr> + * <td align="center">600</td> + * <td align="center">Semi Bold (Demi Bold)</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_SEMI_BOLD}</td> + * </tr> + * <tr> + * <td align="center">700</td> + * <td align="center">Bold</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_BOLD}</td> + * </tr> + * <tr> + * <td align="center">800</td> + * <td align="center">Extra Bold (Ultra Bold)</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_EXTRA_BOLD}</td> + * </tr> + * <tr> + * <td align="center">900</td> + * <td align="center">Black (Heavy)</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_BLACK}</td> + * </tr> + * </tbody> + * </p> + * + * @see FontStyle#FONT_WEIGHT_THIN + * @see FontStyle#FONT_WEIGHT_EXTRA_LIGHT + * @see FontStyle#FONT_WEIGHT_LIGHT + * @see FontStyle#FONT_WEIGHT_NORMAL + * @see FontStyle#FONT_WEIGHT_MEDIUM + * @see FontStyle#FONT_WEIGHT_SEMI_BOLD + * @see FontStyle#FONT_WEIGHT_BOLD + * @see FontStyle#FONT_WEIGHT_EXTRA_BOLD + * @see FontStyle#FONT_WEIGHT_BLACK + * @param weight a weight value + * @return this builder + */ + public @NonNull Builder setWeight( + @IntRange(from = FontStyle.FONT_WEIGHT_MIN, to = FontStyle.FONT_WEIGHT_MAX) + int weight) { + Preconditions.checkArgument( + FontStyle.FONT_WEIGHT_MIN <= weight && weight <= FontStyle.FONT_WEIGHT_MAX); + mWeight = weight; + return this; + } + + /** + * Sets italic information of the font. + * + * Tells the system the style of the given font. If this function is not called, the system + * will resolve the style by reading font tables. + * + * For example, if you want to use italic font as upright font, call {@code + * setSlant(FontStyle.FONT_SLANT_UPRIGHT)} explicitly. + * + * @return this builder + */ + public @NonNull Builder setSlant(@FontStyle.FontSlant int slant) { + mItalic = slant == FontStyle.FONT_SLANT_UPRIGHT ? STYLE_NORMAL : STYLE_ITALIC; + return this; + } + + /** + * Sets an index of the font collection. See {@link android.R.attr#ttcIndex}. + * + * @param ttcIndex An index of the font collection. If the font source is not font + * collection, do not call this method or specify 0. + * @return this builder + */ + public @NonNull Builder setTtcIndex(@IntRange(from = 0) int ttcIndex) { + mTtcIndex = ttcIndex; + return this; + } + + /** + * Sets the font variation settings. + * + * @param variationSettings see {@link FontVariationAxis#fromFontVariationSettings(String)} + * @return this builder + * @throws IllegalArgumentException If given string is not a valid font variation settings + * format. + */ + public @NonNull Builder setFontVariationSettings(@Nullable String variationSettings) { + mAxes = FontVariationAxis.fromFontVariationSettings(variationSettings); + return this; + } + + /** + * Sets the font variation settings. + * + * @param axes an array of font variation axis tag-value pairs + * @return this builder + */ + public @NonNull Builder setFontVariationSettings(@Nullable FontVariationAxis[] axes) { + mAxes = axes == null ? null : axes.clone(); + return this; + } + + /** + * Creates the font based on the configured values. + * @return the Font object + */ + public @Nullable Font build() throws IOException { + if (mException != null) { + throw new IOException("Failed to read font contents", mException); + } + if (mWeight == NOT_SPECIFIED || mItalic == NOT_SPECIFIED) { + final int packed = FontFileUtil.analyzeStyle(mBuffer, mTtcIndex, mAxes); + if (FontFileUtil.isSuccess(packed)) { + if (mWeight == NOT_SPECIFIED) { + mWeight = FontFileUtil.unpackWeight(packed); + } + if (mItalic == NOT_SPECIFIED) { + mItalic = FontFileUtil.unpackItalic(packed) ? STYLE_ITALIC : STYLE_NORMAL; + } + } else { + mWeight = 400; + mItalic = STYLE_NORMAL; + } + } + mWeight = Math.max(FontStyle.FONT_WEIGHT_MIN, + Math.min(FontStyle.FONT_WEIGHT_MAX, mWeight)); + final boolean italic = (mItalic == STYLE_ITALIC); + final int slant = (mItalic == STYLE_ITALIC) + ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT; + final long builderPtr = nInitBuilder(); + if (mAxes != null) { + for (FontVariationAxis axis : mAxes) { + nAddAxis(builderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue()); + } + } + final ByteBuffer readonlyBuffer = mBuffer.asReadOnlyBuffer(); + final String filePath = mFile == null ? "" : mFile.getAbsolutePath(); + final long ptr = nBuild(builderPtr, readonlyBuffer, filePath, mWeight, italic, + mTtcIndex); + final Font font = new Font(ptr, readonlyBuffer, mFile, + new FontStyle(mWeight, slant), mTtcIndex, mAxes, mLocaleList); + sFontRegistory.registerNativeAllocation(font, ptr); + return font; + } + + /** + * Native methods for accessing underlying buffer in Asset + */ + private static native long nGetNativeAsset( + @NonNull AssetManager am, @NonNull String path, boolean isAsset, int cookie); + private static native ByteBuffer nGetAssetBuffer(long nativeAsset); + @CriticalNative + private static native long nGetReleaseNativeAssetFunc(); + + /** + * Native methods for creating Font + */ + private static native long nInitBuilder(); + @CriticalNative + private static native void nAddAxis(long builderPtr, int tag, float value); + private static native long nBuild( + long builderPtr, @NonNull ByteBuffer buffer, @NonNull String filePath, int weight, + boolean italic, int ttcIndex); + @CriticalNative + private static native long nGetReleaseNativeFont(); + } + + private final long mNativePtr; // address of the shared ptr of minikin::Font + private final @NonNull ByteBuffer mBuffer; + private final @Nullable File mFile; + private final FontStyle mFontStyle; + private final @IntRange(from = 0) int mTtcIndex; + private final @Nullable FontVariationAxis[] mAxes; + private final @NonNull String mLocaleList; + + /** + * Use Builder instead + */ + private Font(long nativePtr, @NonNull ByteBuffer buffer, @Nullable File file, + @NonNull FontStyle fontStyle, @IntRange(from = 0) int ttcIndex, + @Nullable FontVariationAxis[] axes, @NonNull String localeList) { + mBuffer = buffer; + mFile = file; + mFontStyle = fontStyle; + mNativePtr = nativePtr; + mTtcIndex = ttcIndex; + mAxes = axes; + mLocaleList = localeList; + } + + /** + * Returns a font file buffer. + * + * @return a font buffer + */ + public @NonNull ByteBuffer getBuffer() { + return mBuffer; + } + + /** + * Returns a file path of this font. + * + * This returns null if this font is not created from regular file. + * + * @return a file path of the font + */ + public @Nullable File getFile() { + return mFile; + } + + /** + * Get a style associated with this font. + * + * @see Builder#setWeight(int) + * @see Builder#setSlant(int) + * @return a font style + */ + public FontStyle getStyle() { + return mFontStyle; + } + + /** + * Get a TTC index value associated with this font. + * + * If TTF/OTF file is provided, this value is always 0. + * + * @see Builder#setTtcIndex(int) + * @return a TTC index value + */ + public @IntRange(from = 0) int getTtcIndex() { + return mTtcIndex; + } + + /** + * Get a font variation settings associated with this font + * + * @see Builder#setFontVariationSettings(String) + * @see Builder#setFontVariationSettings(FontVariationAxis[]) + * @return font variation settings + */ + public @Nullable FontVariationAxis[] getAxes() { + return mAxes == null ? null : mAxes.clone(); + } + + /** + * Get a locale list of this font. + * + * This is always empty if this font is not a system font. + * @return a locale list + */ + public @NonNull LocaleList getLocaleList() { + return LocaleList.forLanguageTags(mLocaleList); + } + + /** @hide */ + public long getNativePtr() { + return mNativePtr; + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (o == null || !(o instanceof Font)) { + return false; + } + Font f = (Font) o; + return mFontStyle.equals(f.mFontStyle) && f.mTtcIndex == mTtcIndex + && Arrays.equals(f.mAxes, mAxes) && f.mBuffer.equals(mBuffer); + } + + @Override + public int hashCode() { + return Objects.hash(mFontStyle, mTtcIndex, Arrays.hashCode(mAxes), mBuffer); + } + + @Override + public String toString() { + return "Font {" + + "path=" + mFile + + ", style=" + mFontStyle + + ", ttcIndex=" + mTtcIndex + + ", axes=" + FontVariationAxis.toFontVariationSettings(mAxes) + + ", localeList=" + mLocaleList + + ", buffer=" + mBuffer + + "}"; + } +} diff --git a/graphics/java/android/graphics/fonts/FontCustomizationParser.java b/graphics/java/android/graphics/fonts/FontCustomizationParser.java new file mode 100644 index 000000000000..0291d7484dc5 --- /dev/null +++ b/graphics/java/android/graphics/fonts/FontCustomizationParser.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2018 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.graphics.FontListParser; +import android.text.FontConfig; +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; + +/** + * Parser for font customization + * + * @hide + */ +public class FontCustomizationParser { + /** + * Represents a customization XML + */ + public static class Result { + ArrayList<FontConfig.Family> mAdditionalNamedFamilies = new ArrayList<>(); + ArrayList<FontConfig.Alias> mAdditionalAliases = new ArrayList<>(); + } + + /** + * Parses the customization XML + * + * Caller must close the input stream + */ + public static Result parse(@NonNull InputStream in, @NonNull String fontDir) + throws XmlPullParserException, IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, null); + parser.nextTag(); + return readFamilies(parser, fontDir); + } + + private static void validate(Result result) { + HashSet<String> familyNames = new HashSet<>(); + for (int i = 0; i < result.mAdditionalNamedFamilies.size(); ++i) { + final FontConfig.Family family = result.mAdditionalNamedFamilies.get(i); + final String name = family.getName(); + if (name == null) { + throw new IllegalArgumentException("new-named-family requires name attribute"); + } + if (!familyNames.add(name)) { + throw new IllegalArgumentException( + "new-named-family requires unique name attribute"); + } + } + } + + private static Result readFamilies(XmlPullParser parser, String fontDir) + throws XmlPullParserException, IOException { + Result out = new Result(); + parser.require(XmlPullParser.START_TAG, null, "fonts-modification"); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) continue; + String tag = parser.getName(); + if (tag.equals("family")) { + readFamily(parser, fontDir, out); + } else if (tag.equals("alias")) { + out.mAdditionalAliases.add(FontListParser.readAlias(parser)); + } else { + FontListParser.skip(parser); + } + } + validate(out); + return out; + } + + private static void readFamily(XmlPullParser parser, String fontDir, Result out) + throws XmlPullParserException, IOException { + final String customizationType = parser.getAttributeValue(null, "customizationType"); + if (customizationType == null) { + throw new IllegalArgumentException("customizationType must be specified"); + } + if (customizationType.equals("new-named-family")) { + out.mAdditionalNamedFamilies.add(FontListParser.readFamily(parser, fontDir)); + } else { + throw new IllegalArgumentException("Unknown customizationType=" + customizationType); + } + } +} diff --git a/graphics/java/android/graphics/fonts/FontFamily.java b/graphics/java/android/graphics/fonts/FontFamily.java new file mode 100644 index 000000000000..c0f1b163ea11 --- /dev/null +++ b/graphics/java/android/graphics/fonts/FontFamily.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2018 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.IntRange; +import android.annotation.NonNull; +import android.text.FontConfig; + +import com.android.internal.util.Preconditions; + +import dalvik.annotation.optimization.CriticalNative; + +import libcore.util.NativeAllocationRegistry; + +import java.util.ArrayList; +import java.util.HashSet; + +/** + * A font family class can be used for creating Typeface. + * + * <p> + * A font family is a bundle of fonts for drawing text in various styles. + * For example, you can bundle regular style font and bold style font into a single font family, + * then system will select the correct style font from family for drawing. + * + * <pre> + * FontFamily family = new FontFamily.Builder(new Font.Builder("regular.ttf").build()) + * .addFont(new Font.Builder("bold.ttf").build()).build(); + * Typeface typeface = new Typeface.Builder2(family).build(); + * + * SpannableStringBuilder ssb = new SpannableStringBuilder("Hello, World."); + * ssb.setSpan(new StyleSpan(Typeface.Bold), 6, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + * + * textView.setTypeface(typeface); + * textView.setText(ssb); + * </pre> + * + * In this example, "Hello, " is drawn with "regular.ttf", and "World." is drawn with "bold.ttf". + * + * If there is no font exactly matches with the text style, the system will select the closest font. + * </p> + * + */ +public final class FontFamily { + private static final String TAG = "FontFamily"; + + /** + * A builder class for creating new FontFamily. + */ + public static class Builder { + private static final NativeAllocationRegistry sFamilyRegistory = + new NativeAllocationRegistry(FontFamily.class.getClassLoader(), + nGetReleaseNativeFamily(), 64); + + private final ArrayList<Font> mFonts = new ArrayList<>(); + private final HashSet<Integer> mStyleHashSet = new HashSet<>(); + + /** + * Constructs a builder. + * + * @param font a font + */ + public Builder(@NonNull Font font) { + Preconditions.checkNotNull(font, "font can not be null"); + mStyleHashSet.add(makeStyleIdentifier(font)); + mFonts.add(font); + } + + /** + * Adds different style font to the builder. + * + * System will select the font if the text style is closest to the font. + * If the same style font is already added to the builder, this method will fail with + * {@link IllegalArgumentException}. + * + * Note that system assumes all fonts bundled in FontFamily have the same coverage for the + * code points. For example, regular style font and bold style font must have the same code + * point coverage, otherwise some character may be shown as tofu. + * + * @param font a font + * @return this builder + */ + public @NonNull Builder addFont(@NonNull Font font) { + Preconditions.checkNotNull(font, "font can not be null"); + if (!mStyleHashSet.add(makeStyleIdentifier(font))) { + throw new IllegalArgumentException(font + " has already been added"); + } + mFonts.add(font); + return this; + } + + /** + * Build the font family + * @return a font family + */ + public @NonNull FontFamily build() { + return build("", FontConfig.Family.VARIANT_DEFAULT, true /* isCustomFallback */); + } + + /** @hide */ + public @NonNull FontFamily build(@NonNull String langTags, int variant, + boolean isCustomFallback) { + final long builderPtr = nInitBuilder(); + for (int i = 0; i < mFonts.size(); ++i) { + nAddFont(builderPtr, mFonts.get(i).getNativePtr()); + } + final long ptr = nBuild(builderPtr, langTags, variant, isCustomFallback); + final FontFamily family = new FontFamily(mFonts, ptr); + sFamilyRegistory.registerNativeAllocation(family, ptr); + return family; + } + + private static int makeStyleIdentifier(@NonNull Font font) { + return font.getStyle().getWeight() | (font.getStyle().getSlant() << 16); + } + + private static native long nInitBuilder(); + @CriticalNative + private static native void nAddFont(long builderPtr, long fontPtr); + private static native long nBuild(long builderPtr, String langTags, int variant, + boolean isCustomFallback); + @CriticalNative + private static native long nGetReleaseNativeFamily(); + } + + private final ArrayList<Font> mFonts; + private final long mNativePtr; + + // Use Builder instead. + private FontFamily(@NonNull ArrayList<Font> fonts, long ptr) { + mFonts = fonts; + mNativePtr = ptr; + } + + /** + * Returns a font + * + * @param index an index of the font + * @return a registered font + */ + public Font getFont(@IntRange(from = 0) int index) { + return mFonts.get(index); + } + + /** + * Returns the number of fonts in this FontFamily. + * + * @return the number of fonts registered in this family. + */ + public int getSize() { + return mFonts.size(); + } + + /** @hide */ + public long getNativePtr() { + return mNativePtr; + } +} diff --git a/graphics/java/android/graphics/fonts/FontFileUtil.java b/graphics/java/android/graphics/fonts/FontFileUtil.java new file mode 100644 index 000000000000..f8b456b982c5 --- /dev/null +++ b/graphics/java/android/graphics/fonts/FontFileUtil.java @@ -0,0 +1,134 @@ +/* + * Copyright 2018 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.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Provides a utility for font file operations. + * @hide + */ +public class FontFileUtil { + + private FontFileUtil() {} // Do not instanciate + + /** + * Unpack the weight value from packed integer. + */ + public static int unpackWeight(int packed) { + return packed & 0xFFFF; + } + + /** + * Unpack the italic value from packed integer. + */ + public static boolean unpackItalic(int packed) { + return (packed & 0x10000) != 0; + } + + /** + * Returns true if the analyzeStyle succeeded + */ + public static boolean isSuccess(int packed) { + return packed != ANALYZE_ERROR; + } + + private static int pack(@IntRange(from = 0, to = 1000) int weight, boolean italic) { + return weight | (italic ? 0x10000 : 0); + } + + private static final int SFNT_VERSION_1 = 0x00010000; + private static final int SFNT_VERSION_OTTO = 0x4F54544F; + private static final int TTC_TAG = 0x74746366; + private static final int OS2_TABLE_TAG = 0x4F532F32; + + private static final int ANALYZE_ERROR = 0xFFFFFFFF; + + /** + * Analyze the font file returns packed style info + */ + public static final int analyzeStyle(@NonNull ByteBuffer buffer, + @IntRange(from = 0) int ttcIndex, @Nullable FontVariationAxis[] varSettings) { + int weight = -1; + int italic = -1; + if (varSettings != null) { + for (FontVariationAxis axis :varSettings) { + if ("wght".equals(axis.getTag())) { + weight = (int) axis.getStyleValue(); + } else if ("ital".equals(axis.getTag())) { + italic = (axis.getStyleValue() == 1.0f) ? 1 : 0; + } + } + } + + if (weight != -1 && italic != -1) { + // Both weight/italic style are specifeid by variation settings. + // No need to look into OS/2 table. + // TODO: Good to look HVAR table to check if this font supports wght/ital axes. + return pack(weight, italic == 1); + } + + ByteOrder originalOrder = buffer.order(); + buffer.order(ByteOrder.BIG_ENDIAN); + try { + int fontFileOffset = 0; + int magicNumber = buffer.getInt(0); + if (magicNumber == TTC_TAG) { + // TTC file. + if (ttcIndex >= buffer.getInt(8 /* offset to number of fonts in TTC */)) { + return ANALYZE_ERROR; + } + fontFileOffset = buffer.getInt( + 12 /* offset to array of offsets of font files */ + 4 * ttcIndex); + } + int sfntVersion = buffer.getInt(fontFileOffset); + + if (sfntVersion != SFNT_VERSION_1 && sfntVersion != SFNT_VERSION_OTTO) { + return ANALYZE_ERROR; + } + + int numTables = buffer.getShort(fontFileOffset + 4 /* offset to number of tables */); + int os2TableOffset = -1; + for (int i = 0; i < numTables; ++i) { + int tableOffset = fontFileOffset + 12 /* size of offset table */ + + i * 16 /* size of table record */; + if (buffer.getInt(tableOffset) == OS2_TABLE_TAG) { + os2TableOffset = buffer.getInt(tableOffset + 8 /* offset to the table */); + break; + } + } + + if (os2TableOffset == -1) { + // Couldn't find OS/2 table. use regular style + return pack(400, false); + } + + int weightFromOS2 = buffer.getShort(os2TableOffset + 4 /* offset to weight class */); + boolean italicFromOS2 = + (buffer.getShort(os2TableOffset + 62 /* offset to fsSelection */) & 1) != 0; + return pack(weight == -1 ? weightFromOS2 : weight, + italic == -1 ? italicFromOS2 : italic == 1); + } finally { + buffer.order(originalOrder); + } + } +} diff --git a/graphics/java/android/graphics/fonts/FontStyle.java b/graphics/java/android/graphics/fonts/FontStyle.java new file mode 100644 index 000000000000..82fc7ac01772 --- /dev/null +++ b/graphics/java/android/graphics/fonts/FontStyle.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2018 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.IntDef; +import android.annotation.IntRange; +import android.annotation.Nullable; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * A font style object. + * + * This class represents a single font style which is a pair of weight value and slant value. + * Here are common font styles examples: + * <p> + * <pre> + * <code> + * final FontStyle NORMAL = new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT); + * final FontStyle BOLD = new FontStyle(FONT_WEIGHT_BOLD, FONT_SLANT_UPRIGHT); + * final FontStyle ITALIC = new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_ITALIC); + * final FontStyle BOLD_ITALIC = new FontStyle(FONT_WEIGHT_BOLD, FONT_SLANT_ITALIC); + * </code> + * </pre> + * </p> + * + */ +public final class FontStyle { + private static final String TAG = "FontStyle"; + + /** + * A minimum weight value for the font + */ + public static final int FONT_WEIGHT_MIN = 1; + + /** + * A font weight value for the thin weight + */ + public static final int FONT_WEIGHT_THIN = 100; + + /** + * A font weight value for the extra-light weight + */ + public static final int FONT_WEIGHT_EXTRA_LIGHT = 200; + + /** + * A font weight value for the light weight + */ + public static final int FONT_WEIGHT_LIGHT = 300; + + /** + * A font weight value for the normal weight + */ + public static final int FONT_WEIGHT_NORMAL = 400; + + /** + * A font weight value for the medium weight + */ + public static final int FONT_WEIGHT_MEDIUM = 500; + + /** + * A font weight value for the semi-bold weight + */ + public static final int FONT_WEIGHT_SEMI_BOLD = 600; + + /** + * A font weight value for the bold weight. + */ + public static final int FONT_WEIGHT_BOLD = 700; + + /** + * A font weight value for the extra-bold weight + */ + public static final int FONT_WEIGHT_EXTRA_BOLD = 800; + + /** + * A font weight value for the black weight + */ + public static final int FONT_WEIGHT_BLACK = 900; + + /** + * A maximum weight value for the font + */ + public static final int FONT_WEIGHT_MAX = 1000; + + /** + * A font slant value for upright + */ + public static final int FONT_SLANT_UPRIGHT = 0; + + /** + * A font slant value for italic + */ + public static final int FONT_SLANT_ITALIC = 1; + + // TODO: Support FONT_SLANT_OBLIQUE + + /** @hide */ + @IntDef(prefix = { "FONT_SLANT_" }, value = { + FONT_SLANT_UPRIGHT, + FONT_SLANT_ITALIC + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FontSlant {} + + private final @IntRange(from = 0, to = 1000) int mWeight; + private final @FontSlant int mSlant; + // TODO: Support width + + public FontStyle() { + mWeight = FONT_WEIGHT_NORMAL; + mSlant = FONT_SLANT_UPRIGHT; + } + + /** + * Create FontStyle with specific weight and italic + * + * <p> + * <table> + * <thead> + * <tr> + * <th align="center">Value</th> + * <th align="center">Name</th> + * <th align="center">Android Definition</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">100</td> + * <td align="center">Thin</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_THIN}</td> + * </tr> + * <tr> + * <td align="center">200</td> + * <td align="center">Extra Light (Ultra Light)</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_EXTRA_LIGHT}</td> + * </tr> + * <tr> + * <td align="center">300</td> + * <td align="center">Light</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_LIGHT}</td> + * </tr> + * <tr> + * <td align="center">400</td> + * <td align="center">Normal (Regular)</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_NORMAL}</td> + * </tr> + * <tr> + * <td align="center">500</td> + * <td align="center">Medium</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_MEDIUM}</td> + * </tr> + * <tr> + * <td align="center">600</td> + * <td align="center">Semi Bold (Demi Bold)</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_SEMI_BOLD}</td> + * </tr> + * <tr> + * <td align="center">700</td> + * <td align="center">Bold</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_BOLD}</td> + * </tr> + * <tr> + * <td align="center">800</td> + * <td align="center">Extra Bold (Ultra Bold)</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_EXTRA_BOLD}</td> + * </tr> + * <tr> + * <td align="center">900</td> + * <td align="center">Black (Heavy)</td> + * <td align="center">{@link FontStyle#FONT_WEIGHT_BLACK}</td> + * </tr> + * </tbody> + * </p> + * + * @see FontStyle#FONT_WEIGHT_THIN + * @see FontStyle#FONT_WEIGHT_EXTRA_LIGHT + * @see FontStyle#FONT_WEIGHT_LIGHT + * @see FontStyle#FONT_WEIGHT_NORMAL + * @see FontStyle#FONT_WEIGHT_MEDIUM + * @see FontStyle#FONT_WEIGHT_SEMI_BOLD + * @see FontStyle#FONT_WEIGHT_BOLD + * @see FontStyle#FONT_WEIGHT_EXTRA_BOLD + * @see FontStyle#FONT_WEIGHT_BLACK + * @param weight a weight value + * @param slant a slant value + */ + public FontStyle(int weight, @FontSlant int slant) { + Preconditions.checkArgument(FONT_WEIGHT_MIN <= weight && weight <= FONT_WEIGHT_MAX, + "weight value must be [" + FONT_WEIGHT_MIN + ", " + FONT_WEIGHT_MAX + "]"); + Preconditions.checkArgument(slant == FONT_SLANT_UPRIGHT || slant == FONT_SLANT_ITALIC, + "slant value must be FONT_SLANT_UPRIGHT or FONT_SLANT_UPRIGHT"); + mWeight = weight; + mSlant = slant; + } + + + /** + * Gets the weight value + * + * @see FontStyle#setWeight(int) + * @return a weight value + */ + public @IntRange(from = 0, to = 1000) int getWeight() { + return mWeight; + } + + /** + * Gets the slant value + * + * @return a slant value + */ + public @FontSlant int getSlant() { + return mSlant; + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (o == null || !(o instanceof FontStyle)) { + return false; + } + FontStyle fontStyle = (FontStyle) o; + return fontStyle.mWeight == mWeight && fontStyle.mSlant == mSlant; + } + + @Override + public int hashCode() { + return Objects.hash(mWeight, mSlant); + } + + @Override + public String toString() { + return "FontStyle { weight=" + mWeight + ", slant=" + mSlant + "}"; + } +} diff --git a/graphics/java/android/graphics/fonts/FontVariationAxis.java b/graphics/java/android/graphics/fonts/FontVariationAxis.java index a240a78d4454..bcee559d8291 100644 --- a/graphics/java/android/graphics/fonts/FontVariationAxis.java +++ b/graphics/java/android/graphics/fonts/FontVariationAxis.java @@ -23,6 +23,7 @@ import android.os.Build; import android.text.TextUtils; import java.util.ArrayList; +import java.util.Objects; import java.util.regex.Pattern; /** @@ -187,5 +188,22 @@ public final class FontVariationAxis { } return TextUtils.join(",", axes); } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (o == null || !(o instanceof FontVariationAxis)) { + return false; + } + FontVariationAxis axis = (FontVariationAxis) o; + return axis.mTag == mTag && axis.mStyleValue == mStyleValue; + } + + @Override + public int hashCode() { + return Objects.hash(mTag, mStyleValue); + } } diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java new file mode 100644 index 000000000000..4a9cf14d04a5 --- /dev/null +++ b/graphics/java/android/graphics/fonts/SystemFonts.java @@ -0,0 +1,319 @@ +/* + * Copyright 2018 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.graphics.FontListParser; +import android.text.FontConfig; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Provides the system font configurations. + */ +public final class SystemFonts { + private static final String TAG = "SystemFonts"; + private static final String DEFAULT_FAMILY = "sans-serif"; + + private SystemFonts() {} // Do not instansiate. + + private static final Map<String, FontFamily[]> sSystemFallbackMap; + private static final FontConfig.Alias[] sAliases; + private static final List<Font> sAvailableFonts; + + /** + * Returns all available font files in the system. + * + * @return a set of system fonts + */ + public static @NonNull Set<Font> getAvailableFonts() { + HashSet<Font> set = new HashSet<>(); + set.addAll(sAvailableFonts); + return set; + } + + /** + * Returns fallback list for the given family name. + * + * If no fallback found for the given family name, returns fallback for the default family. + * + * @param familyName family name, e.g. "serif" + * @hide + */ + public static @NonNull FontFamily[] getSystemFallback(@Nullable String familyName) { + final FontFamily[] families = sSystemFallbackMap.get(familyName); + return families == null ? sSystemFallbackMap.get(DEFAULT_FAMILY) : families; + } + + /** + * Returns raw system fallback map. + * + * This method is intended to be used only by Typeface static initializer. + * @hide + */ + public static @NonNull Map<String, FontFamily[]> getRawSystemFallbackMap() { + return sSystemFallbackMap; + } + + /** + * Returns a list of aliases. + * + * This method is intended to be used only by Typeface static initializer. + * @hide + */ + public static @NonNull FontConfig.Alias[] getAliases() { + return sAliases; + } + + private static @Nullable ByteBuffer mmap(@NonNull 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 void pushFamilyToFallback(@NonNull FontConfig.Family xmlFamily, + @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap, + @NonNull Map<String, ByteBuffer> cache, + @NonNull ArrayList<Font> availableFonts) { + + 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, availableFonts); + + // 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, + availableFonts); + if (family != null) { + fallbackMap.valueAt(i).add(family); + } else if (defaultFamily != null) { + fallbackMap.valueAt(i).add(defaultFamily); + } else { + // There is no valid for for default fallback. Ignore. + } + } + } + } + + private static @Nullable FontFamily createFontFamily(@NonNull String familyName, + @NonNull List<FontConfig.Font> fonts, + @NonNull String languageTags, + @FontConfig.Family.Variant int variant, + @NonNull Map<String, ByteBuffer> cache, + @NonNull ArrayList<Font> availableFonts) { + if (fonts.size() == 0) { + return null; + } + + FontFamily.Builder b = null; + for (int i = 0; i < fonts.size(); i++) { + final FontConfig.Font fontConfig = fonts.get(i); + final String fullPath = fontConfig.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; + } + } + + final Font font; + try { + font = new Font.Builder(buffer, new File(fullPath), languageTags) + .setWeight(fontConfig.getWeight()) + .setSlant(fontConfig.isItalic() ? FontStyle.FONT_SLANT_ITALIC + : FontStyle.FONT_SLANT_UPRIGHT) + .setTtcIndex(fontConfig.getTtcIndex()) + .setFontVariationSettings(fontConfig.getAxes()) + .build(); + } catch (IOException e) { + throw new RuntimeException(e); // Never reaches here + } + + availableFonts.add(font); + if (b == null) { + b = new FontFamily.Builder(font); + } else { + b.addFont(font); + } + } + return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */); + } + + private static void appendNamedFamily(@NonNull FontConfig.Family xmlFamily, + @NonNull HashMap<String, ByteBuffer> bufferCache, + @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap, + @NonNull ArrayList<Font> availableFonts) { + final String familyName = xmlFamily.getName(); + final FontFamily family = createFontFamily( + familyName, Arrays.asList(xmlFamily.getFonts()), + xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, availableFonts); + if (family == null) { + return; + } + final ArrayList<FontFamily> fallback = new ArrayList<>(); + fallback.add(family); + fallbackListMap.put(familyName, fallback); + } + + /** + * 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 fallbackMap An output system fallback map. Caller must pass empty map. + * @return a list of aliases + * @hide + */ + @VisibleForTesting + public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath, + @NonNull String fontDir, + @NonNull FontCustomizationParser.Result oemCustomization, + @NonNull ArrayMap<String, FontFamily[]> fallbackMap, + @NonNull ArrayList<Font> availableFonts) { + try { + final FileInputStream fontsIn = new FileInputStream(xmlPath); + final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir); + + 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; + } + appendNamedFamily(xmlFamily, bufferCache, fallbackListMap, availableFonts); + } + + for (int i = 0; i < oemCustomization.mAdditionalNamedFamilies.size(); ++i) { + appendNamedFamily(oemCustomization.mAdditionalNamedFamilies.get(i), + bufferCache, fallbackListMap, availableFonts); + } + + // 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, availableFonts); + } + } + + // 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 ArrayList<FontConfig.Alias> list = new ArrayList<>(); + list.addAll(Arrays.asList(fontConfig.getAliases())); + list.addAll(oemCustomization.mAdditionalAliases); + return list.toArray(new FontConfig.Alias[list.size()]); + } catch (IOException | XmlPullParserException e) { + Log.e(TAG, "Failed initialize system fallbacks.", e); + return ArrayUtils.emptyArray(FontConfig.Alias.class); + } + } + + private static FontCustomizationParser.Result readFontCustomization( + @NonNull String customizeXml, @NonNull String customFontsDir) { + try (FileInputStream f = new FileInputStream(customizeXml)) { + return FontCustomizationParser.parse(f, customFontsDir); + } catch (IOException e) { + return new FontCustomizationParser.Result(); + } catch (XmlPullParserException e) { + Log.e(TAG, "Failed to parse font customization XML", e); + return new FontCustomizationParser.Result(); + } + } + + static { + final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>(); + final ArrayList<Font> availableFonts = new ArrayList<>(); + final FontCustomizationParser.Result oemCustomization = + readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/"); + sAliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", + oemCustomization, systemFallbackMap, availableFonts); + sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap); + sAvailableFonts = Collections.unmodifiableList(availableFonts); + } +} diff --git a/graphics/java/android/graphics/text/LineBreaker.java b/graphics/java/android/graphics/text/LineBreaker.java new file mode 100644 index 000000000000..16479095a63a --- /dev/null +++ b/graphics/java/android/graphics/text/LineBreaker.java @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2018 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.text; + +import android.annotation.FloatRange; +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.Px; + +import dalvik.annotation.optimization.CriticalNative; +import dalvik.annotation.optimization.FastNative; + +import libcore.util.NativeAllocationRegistry; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Provides automatic line breaking for a <em>single</em> paragraph. + * + * <p> + * <pre> + * <code> + * Paint paint = new Paint(); + * Paint bigPaint = new Paint(); + * bigPaint.setTextSize(paint.getTextSize() * 2.0); + * String text = "Hello, Android."; + * + * // Prepare the measured text + * MeasuredText mt = new MeasuredText.Builder(text.toCharArray()) + * .appendStyleRun(paint, 7, false) // Use paint for "Hello, " + * .appednStyleRun(bigPaint, 8, false) // Use bigPaint for "Hello, " + * .build(); + * + * LineBreaker lb = new LineBreaker.Builder() + * // Use simple line breaker + * .setBreakStrategy(LineBreaker.BREAK_STRATEGY_SIMPLE) + * // Do not add hyphenation. + * .setHyphenationFrequency(LineBreaker.HYPHENATION_FREQUENCY_NONE) + * // Build the LineBreaker + * .build(); + * + * ParagraphConstraints c = new ParagraphConstraints(); + * c.setWidth(240); // Set the line wieth as 1024px + * + * // Do the line breaking + * Result r = lb.computeLineBreaks(mt, c, 0); + * + * // Compute the total height of the text. + * float totalHeight = 0; + * for (int i = 0; i < r.getLineCount(); ++i) { // iterate over the lines + * totalHeight += r.getLineDescent(i) - r.getLineAscent(i); + * } + * + * // Draw text to the canvas + * Bitmap bmp = new Bitmap.createBitmap(240, totalHeight, Bitmap.Config.ARGB_8888); + * Canvas c = new Canvas(bmp); + * float yOffset = 0f; + * int prevOffset = 0; + * for (int i = 0; i < r.getLineCount(); ++i) { // iterate over the lines + * int nextOffset = r.getLineBreakOffset(i); + * c.drawText(text, prevOffset, nextOffset, 0f, yOffset, paint); + * + * prevOffset = nextOffset; + * yOffset += r.getLineDescent(i) - r.getLineAscent(i); + * } + * </code> + * </pre> + * </p> + */ +public class LineBreaker { + /** @hide */ + @IntDef(prefix = { "BREAK_STRATEGY_" }, value = { + BREAK_STRATEGY_SIMPLE, + BREAK_STRATEGY_HIGH_QUALITY, + BREAK_STRATEGY_BALANCED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface BreakStrategy {} + + /** + * Value for break strategy indicating simple line breaking. + * + * The line breaker puts words to the line as much as possible and breaks line if no more words + * can fit into the same line. Automatic hyphens are only added when a line has a single word + * and that word is longer than line width. This is the fastest break strategy and ideal for + * editor. + */ + public static final int BREAK_STRATEGY_SIMPLE = 0; + + /** + * Value for break strategy indicating high quality line breaking. + * + * With this option line breaker does whole-paragraph optimization for more readable text, and + * also applies automatic hyphenation when required. + */ + public static final int BREAK_STRATEGY_HIGH_QUALITY = 1; + + /** + * Value for break strategy indicating balanced line breaking. + * + * The line breaker does whole-paragraph optimization for making all lines similar length, and + * also applies automatic hyphenation when required. This break strategy is good for small + * screen devices such as watch screens. + */ + public static final int BREAK_STRATEGY_BALANCED = 2; + + /** @hide */ + @IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = { + HYPHENATION_FREQUENCY_NORMAL, + HYPHENATION_FREQUENCY_FULL, + HYPHENATION_FREQUENCY_NONE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface HyphenationFrequency {} + + /** + * Value for hyphenation frequency indicating no automatic hyphenation. + * + * Using this option disables auto hyphenation which results in better text layout performance. + * A word may be broken without hyphens when a line has a single word and that word is longer + * than line width. Soft hyphens are ignored and will not be used as suggestions for potential + * line breaks. + */ + public static final int HYPHENATION_FREQUENCY_NONE = 0; + + /** + * Value for hyphenation frequency indicating a light amount of automatic hyphenation. + * + * This hyphenation frequency is useful for informal cases, such as short sentences or chat + * messages. + */ + public static final int HYPHENATION_FREQUENCY_NORMAL = 1; + + /** + * Value for hyphenation frequency indicating the full amount of automatic hyphenation. + * + * This hyphenation frequency is useful for running text and where it's important to put the + * maximum amount of text in a screen with limited space. + */ + public static final int HYPHENATION_FREQUENCY_FULL = 2; + + /** @hide */ + @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = { + JUSTIFICATION_MODE_NONE, + JUSTIFICATION_MODE_INTER_WORD + }) + @Retention(RetentionPolicy.SOURCE) + public @interface JustificationMode {} + + /** + * Value for justification mode indicating no justification. + */ + public static final int JUSTIFICATION_MODE_NONE = 0; + + /** + * Value for justification mode indicating the text is justified by stretching word spacing. + */ + public static final int JUSTIFICATION_MODE_INTER_WORD = 1; + + /** + * Helper class for creating a {@link LineBreaker}. + */ + public static class Builder { + private @BreakStrategy int mBreakStrategy = BREAK_STRATEGY_SIMPLE; + private @HyphenationFrequency int mHyphenationFrequency = HYPHENATION_FREQUENCY_NONE; + private @JustificationMode int mJustified = JUSTIFICATION_MODE_NONE; + private @Nullable int[] mIndents = null; + + /** + * Set break strategy. + * + * You can change the line breaking behavior by setting break strategy. The default value is + * {@link #BREAK_STRATEGY_SIMPLE}. + */ + public Builder setBreakStrategy(@BreakStrategy int breakStrategy) { + mBreakStrategy = breakStrategy; + return this; + } + + /** + * Set hyphenation frequency. + * + * You can change the amount of automatic hyphenation used. The default value is + * {@link #HYPHENATION_FREQUENCY_NONE}. + */ + public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) { + mHyphenationFrequency = hyphenationFrequency; + return this; + } + + /** + * Set whether the text is justified. + * + * By setting {@link #JUSTIFICATION_MODE_INTER_WORD}, the line breaker will change the + * internal parameters for justification. + * The default value is {@link #JUSTIFICATION_MODE_NONE} + */ + public Builder setJustified(@JustificationMode int justified) { + mJustified = justified; + return this; + } + + /** + * Set indents. + * + * The supplied array provides the total amount of indentation per line, in pixel. This + * amount is the sum of both left and right indentations. For lines past the last element in + * the array, the indentation amount of the last element is used. + */ + public Builder setIndents(@Nullable int[] indents) { + mIndents = indents; + return this; + } + + /** + * Build a new LineBreaker with given parameters. + * + * You can reuse the Builder instance even after calling this method. + */ + public LineBreaker build() { + return new LineBreaker(mBreakStrategy, mHyphenationFrequency, mJustified, mIndents); + } + } + + /** + * Line breaking constraints for single paragraph. + */ + public static class ParagraphConstraints { + private @FloatRange(from = 0.0f) float mWidth = 0; + private @FloatRange(from = 0.0f) float mFirstWidth = 0; + private @IntRange(from = 0) int mFirstWidthLineCount = 0; + private @Nullable int[] mVariableTabStops = null; + private @IntRange(from = 0) int mDefaultTabStop = 0; + + public ParagraphConstraints() {} + + /** + * Set width for this paragraph. + * + * @see #getWidth() + */ + public void setWidth(@Px @FloatRange(from = 0.0f) float width) { + mWidth = width; + } + + /** + * Set indent for this paragraph. + * + * @param firstWidth the line width of the starting of the paragraph + * @param firstWidthLineCount the number of lines that applies the firstWidth + * @see #getFirstWidth() + * @see #getFirstWidthLineCount() + */ + public void setIndent(@Px @FloatRange(from = 0.0f) float firstWidth, + @Px @IntRange(from = 0) int firstWidthLineCount) { + mFirstWidth = firstWidth; + mFirstWidthLineCount = firstWidthLineCount; + } + + /** + * Set tab stops for this paragraph. + * + * @param tabStops the array of pixels of tap stopping position + * @param defaultTabStop pixels of the default tab stopping position + * @see #getTabStops() + * @see #getDefaultTabStop() + */ + public void setTabStops(@Nullable int[] tabStops, + @Px @IntRange(from = 0) int defaultTabStop) { + mVariableTabStops = tabStops; + mDefaultTabStop = defaultTabStop; + } + + /** + * Return the width for this paragraph in pixels. + * + * @see #setWidth(float) + */ + public @Px @FloatRange(from = 0.0f) float getWidth() { + return mWidth; + } + + /** + * Return the first line's width for this paragraph in pixel. + * + * @see #setIndent(float, int) + */ + public @Px @FloatRange(from = 0.0f) float getFirstWidth() { + return mFirstWidth; + } + + /** + * Return the number of lines to apply the first line's width. + * + * @see #setIndent(float, int) + */ + public @Px @IntRange(from = 0) int getFirstWidthLineCount() { + return mFirstWidthLineCount; + } + + /** + * Returns the array of tab stops in pixels. + * + * @see #setTabStops(int[], int) + */ + public @Nullable int[] getTabStops() { + return mVariableTabStops; + } + + /** + * Returns the default tab stops in pixels. + * + * @see #setTabStop(int[], int) + */ + public @Px @IntRange(from = 0) int getDefaultTabStop() { + return mDefaultTabStop; + } + } + + /** + * Holds the result of the {@link LineBreaker#computeLineBreaks line breaking algorithm}. + * @see LineBreaker#computeLineBreaks + */ + public static class Result { + // Following two contstant must be synced with minikin's line breaker. + private static final int TAB_MASK = 0x20000000; + private static final int HYPHEN_MASK = 0xFF; + + private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry( + Result.class.getClassLoader(), nGetReleaseResultFunc(), 32); + private final long mPtr; + + private Result(long ptr) { + mPtr = ptr; + sRegistry.registerNativeAllocation(this, mPtr); + } + + /** + * Returns the number of lines in the paragraph. + * + * @return number of lines + */ + public @IntRange(from = 0) int getLineCount() { + return nGetLineCount(mPtr); + } + + /** + * Returns character offset of the break for a given line. + * + * @param lineIndex an index of the line. + * @return the break offset. + */ + public @IntRange(from = 0) int getLineBreakOffset(@IntRange(from = 0) int lineIndex) { + return nGetLineBreakOffset(mPtr, lineIndex); + } + + /** + * Returns width of a given line in pixels. + * + * @param lineIndex an index of the line. + * @return width of the line in pixels + */ + public @Px float getLineWidth(@IntRange(from = 0) int lineIndex) { + return nGetLineWidth(mPtr, lineIndex); + } + + /** + * Returns font ascent of the line in pixels. + * + * @param lineIndex an index of the line. + * @return an entier font ascent of the line in pixels. + */ + public @Px float getLineAscent(@IntRange(from = 0) int lineIndex) { + return nGetLineAscent(mPtr, lineIndex); + } + + /** + * Returns font descent of the line in pixels. + * + * @param lineIndex an index of the line. + * @return an entier font descent of the line in pixels. + */ + public @Px float getLineDescent(@IntRange(from = 0) int lineIndex) { + return nGetLineDescent(mPtr, lineIndex); + } + + /** + * Returns true if the line has a TAB character. + * + * @param lineIndex an index of the line. + * @return true if the line has a TAB character + */ + public boolean hasLineTab(int lineIndex) { + return (nGetLineFlag(mPtr, lineIndex) & TAB_MASK) != 0; + } + + /** + * Returns a packed packed hyphen edit for the line. + * + * @param lineIndex an index of the line. + * @return a packed hyphen edit for the line. + * + * @see android.text.Hyphenator#unpackStartHyphenEdit(int) + * @see android.text.Hyphenator#unpackEndHyphenEdit(int) + * @see android.text.Hyphenator#packHyphenEdit(int,int) + */ + public int getLineHyphenEdit(int lineIndex) { + return (nGetLineFlag(mPtr, lineIndex) & HYPHEN_MASK); + } + } + + private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry( + LineBreaker.class.getClassLoader(), nGetReleaseFunc(), 64); + + private final long mNativePtr; + + /** + * Use Builder instead. + */ + private LineBreaker(@BreakStrategy int breakStrategy, + @HyphenationFrequency int hyphenationFrequency, @JustificationMode int justify, + @Nullable int[] indents) { + mNativePtr = nInit(breakStrategy, hyphenationFrequency, + justify == JUSTIFICATION_MODE_INTER_WORD, indents); + sRegistry.registerNativeAllocation(this, mNativePtr); + } + + /** + * Break paragraph into lines. + * + * The result is filled to out param. + * + * @param measuredPara a result of the text measurement + * @param constraints for a single paragraph + * @param lineNumber a line number of this paragraph + */ + public Result computeLineBreaks( + @NonNull MeasuredText measuredPara, + @NonNull ParagraphConstraints constraints, + @IntRange(from = 0) int lineNumber) { + return new Result(nComputeLineBreaks( + mNativePtr, + + // Inputs + measuredPara.getChars(), + measuredPara.getNativePtr(), + measuredPara.getChars().length, + constraints.mFirstWidth, + constraints.mFirstWidthLineCount, + constraints.mWidth, + constraints.mVariableTabStops, + constraints.mDefaultTabStop, + lineNumber)); + } + + @FastNative + private static native long nInit(@BreakStrategy int breakStrategy, + @HyphenationFrequency int hyphenationFrequency, boolean isJustified, + @Nullable int[] indents); + + @CriticalNative + private static native long nGetReleaseFunc(); + + // populates LineBreaks and returns the number of breaks found + // + // the arrays inside the LineBreaks objects are passed in as well + // to reduce the number of JNI calls in the common case where the + // arrays do not have to be resized + // The individual character widths will be returned in charWidths. The length of + // charWidths must be at least the length of the text. + private static native long nComputeLineBreaks( + /* non zero */ long nativePtr, + + // Inputs + @NonNull char[] text, + /* Non Zero */ long measuredTextPtr, + @IntRange(from = 0) int length, + @FloatRange(from = 0.0f) float firstWidth, + @IntRange(from = 0) int firstWidthLineCount, + @FloatRange(from = 0.0f) float restWidth, + @Nullable int[] variableTabStops, + int defaultTabStop, + @IntRange(from = 0) int indentsOffset); + + // Result accessors + @CriticalNative + private static native int nGetLineCount(long ptr); + @CriticalNative + private static native int nGetLineBreakOffset(long ptr, int idx); + @CriticalNative + private static native float nGetLineWidth(long ptr, int idx); + @CriticalNative + private static native float nGetLineAscent(long ptr, int idx); + @CriticalNative + private static native float nGetLineDescent(long ptr, int idx); + @CriticalNative + private static native int nGetLineFlag(long ptr, int idx); + @CriticalNative + private static native long nGetReleaseResultFunc(); +} diff --git a/graphics/java/android/graphics/text/MeasuredText.java b/graphics/java/android/graphics/text/MeasuredText.java new file mode 100644 index 000000000000..6f786517ba12 --- /dev/null +++ b/graphics/java/android/graphics/text/MeasuredText.java @@ -0,0 +1,348 @@ +/* + * 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.text; + +import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Px; +import android.graphics.Paint; +import android.graphics.Rect; + +import com.android.internal.util.Preconditions; + +import dalvik.annotation.optimization.CriticalNative; + +import libcore.util.NativeAllocationRegistry; + +/** + * Result of text shaping of the single paragraph string. + * + * <p> + * <pre> + * <code> + * Paint paint = new Paint(); + * Paint bigPaint = new Paint(); + * bigPaint.setTextSize(paint.getTextSize() * 2.0); + * String text = "Hello, Android."; + * MeasuredText mt = new MeasuredText.Builder(text.toCharArray()) + * .appendStyleRun(paint, 7, false) // Use paint for "Hello, " + * .appendStyleRun(bigPaint, 8, false) // Use bigPaint for "Hello, " + * .build(); + * </code> + * </pre> + * </p> + */ +public class MeasuredText { + private long mNativePtr; + private @NonNull char[] mChars; + + // Use builder instead. + private MeasuredText(long ptr, @NonNull char[] chars) { + mNativePtr = ptr; + mChars = chars; + } + + /** + * Returns the characters in the paragraph used to compute this MeasuredText instance. + */ + public @NonNull char[] getChars() { + return mChars; + } + + /** + * Returns the width of a given range. + * + * @param start an inclusive start index of the range + * @param end an exclusive end index of the range + */ + public @FloatRange(from = 0.0) @Px float getWidth( + @IntRange(from = 0) int start, @IntRange(from = 0) int end) { + Preconditions.checkArgument(0 <= start && start <= mChars.length, + "start(" + start + ") must be 0 <= start <= " + mChars.length); + Preconditions.checkArgument(0 <= end && end <= mChars.length, + "end(" + end + ") must be 0 <= end <= " + mChars.length); + Preconditions.checkArgument(start <= end, + "start(" + start + ") is larger than end(" + end + ")"); + return nGetWidth(mNativePtr, start, end); + } + + /** + * Returns a memory usage of the native object. + * + * @hide + */ + public int getMemoryUsage() { + return nGetMemoryUsage(mNativePtr); + } + + /** + * Retrieves the boundary box of the given range + * + * @param start an inclusive start index of the range + * @param end an exclusive end index of the range + * @param rect an output parameter + */ + public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, + @NonNull Rect rect) { + Preconditions.checkArgument(0 <= start && start <= mChars.length, + "start(" + start + ") must be 0 <= start <= " + mChars.length); + Preconditions.checkArgument(0 <= end && end <= mChars.length, + "end(" + end + ") must be 0 <= end <= " + mChars.length); + Preconditions.checkArgument(start <= end, + "start(" + start + ") is larger than end(" + end + ")"); + Preconditions.checkNotNull(rect); + nGetBounds(mNativePtr, mChars, start, end, rect); + } + + /** + * Returns the width of the character at the given offset. + * + * @param offset an offset of the character. + */ + public @FloatRange(from = 0.0f) @Px float getCharWidthAt(@IntRange(from = 0) int offset) { + Preconditions.checkArgument(0 <= offset && offset < mChars.length, + "offset(" + offset + ") is larger than text length: " + mChars.length); + return nGetCharWidthAt(mNativePtr, offset); + } + + /** + * Returns a native pointer of the underlying native object. + * + * @hide + */ + public long getNativePtr() { + return mNativePtr; + } + + @CriticalNative + private static native float nGetWidth(/* Non Zero */ long nativePtr, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end); + + @CriticalNative + private static native /* Non Zero */ long nGetReleaseFunc(); + + @CriticalNative + private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr); + + private static native void nGetBounds(long nativePtr, char[] buf, int start, int end, + Rect rect); + + @CriticalNative + private static native float nGetCharWidthAt(long nativePtr, int offset); + + /** + * Helper class for creating a {@link MeasuredText}. + * <p> + * <pre> + * <code> + * Paint paint = new Paint(); + * String text = "Hello, Android."; + * MeasuredText mt = new MeasuredText.Builder(text.toCharArray()) + * .appendStyleRun(paint, text.length, false) + * .build(); + * </code> + * </pre> + * </p> + * + * Note: The appendStyle and appendReplacementRun should be called to cover the text length. + */ + public static class Builder { + private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry( + MeasuredText.class.getClassLoader(), nGetReleaseFunc(), 1024); + + private long mNativePtr; + + private final @NonNull char[] mText; + private boolean mComputeHyphenation = false; + private boolean mComputeLayout = true; + private int mCurrentOffset = 0; + + /** + * Construct a builder. + * + * The MeasuredText returned by build method will hold a reference of the text. Developer is + * not supposed to modify the text. + * + * @param text a text + */ + public Builder(@NonNull char[] text) { + Preconditions.checkNotNull(text); + mText = text; + mNativePtr = nInitBuilder(); + } + + /** + * Apply styles to the given length. + * + * Keeps an internal offset which increases at every append. The initial value for this + * offset is zero. After the style is applied the internal offset is moved to {@code offset + * + length}, and next call will start from this new position. + * + * @param paint a paint + * @param length a length to be applied with a given paint, can not exceed the length of the + * text + * @param isRtl true if the text is in RTL context, otherwise false. + */ + public Builder appendStyleRun(@NonNull Paint paint, @IntRange(from = 0) int length, + boolean isRtl) { + Preconditions.checkNotNull(paint); + Preconditions.checkArgument(length > 0, "length can not be negative"); + final int end = mCurrentOffset + length; + Preconditions.checkArgument(end <= mText.length, "Style exceeds the text length"); + nAddStyleRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, isRtl); + mCurrentOffset = end; + return this; + } + + /** + * Used to inform the text layout that the given length is replaced with the object of given + * width. + * + * Keeps an internal offset which increases at every append. The initial value for this + * offset is zero. After the style is applied the internal offset is moved to {@code offset + * + length}, and next call will start from this new position. + * + * Informs the layout engine that the given length should not be processed, instead the + * provided width should be used for calculating the width of that range. + * + * @param length a length to be replaced with the object, can not exceed the length of the + * text + * @param width a replacement width of the range + */ + public Builder appendReplacementRun(@NonNull Paint paint, + @IntRange(from = 0) int length, @FloatRange(from = 0) float width) { + Preconditions.checkArgument(length > 0, "length can not be negative"); + final int end = mCurrentOffset + length; + Preconditions.checkArgument(end <= mText.length, "Replacement exceeds the text length"); + nAddReplacementRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, width); + mCurrentOffset = end; + return this; + } + + /** + * By passing true to this method, the build method will compute all possible hyphenation + * pieces as well. + * + * If you don't want to use automatic hyphenation, you can pass false to this method and + * save the computation time of hyphenation. The default value is false. + * + * Even if you pass false to this method, you can still enable automatic hyphenation of + * LineBreaker but line break computation becomes slower. + * + * @param computeHyphenation true if you want to use automatic hyphenations. + */ + public Builder setComputeHyphenation(boolean computeHyphenation) { + mComputeHyphenation = computeHyphenation; + return this; + } + + /** + * By passing true to this method, the build method will compute all full layout + * information. + * + * If you don't use {@link MeasuredText#getBounds(int,int,android.graphics.Rect)}, you can + * pass false to this method and save the memory spaces. The default value is true. + * + * Even if you pass false to this method, you can still call getBounds but it becomes + * slower. + * + * @param computeLayout true if you want to retrieve full layout info, e.g. bbox. + */ + public Builder setComputeLayout(boolean computeLayout) { + mComputeLayout = computeLayout; + return this; + } + + /** + * Creates a MeasuredText. + * + * Once you called build() method, you can't reuse the Builder class again. + * @throws IllegalStateException if this Builder is reused. + * @throws IllegalStateException if the whole text is not covered by one or more runs (style + * or replacement) + */ + public MeasuredText build() { + ensureNativePtrNoReuse(); + if (mCurrentOffset != mText.length) { + throw new IllegalStateException("Style info has not been provided for all text."); + } + try { + long ptr = nBuildMeasuredText(mNativePtr, mText, mComputeHyphenation, + mComputeLayout); + MeasuredText res = new MeasuredText(ptr, mText); + sRegistry.registerNativeAllocation(res, ptr); + return res; + } finally { + nFreeBuilder(mNativePtr); + mNativePtr = 0; + } + } + + /** + * Ensures {@link #mNativePtr} is not reused. + * + * <p/> This is a method by itself to help increase testability - eg. Robolectric might want + * to override the validation behavior in test environment. + */ + private void ensureNativePtrNoReuse() { + if (mNativePtr == 0) { + throw new IllegalStateException("Builder can not be reused."); + } + } + + private static native /* Non Zero */ long nInitBuilder(); + + /** + * Apply style to make native measured text. + * + * @param nativeBuilderPtr The native MeasuredParagraph builder pointer. + * @param paintPtr The native paint pointer to be applied. + * @param start The start offset in the copied buffer. + * @param end The end offset in the copied buffer. + * @param isRtl True if the text is RTL. + */ + private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr, + /* Non Zero */ long paintPtr, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + boolean isRtl); + /** + * Apply ReplacementRun to make native measured text. + * + * @param nativeBuilderPtr The native MeasuredParagraph builder pointer. + * @param paintPtr The native paint pointer to be applied. + * @param start The start offset in the copied buffer. + * @param end The end offset in the copied buffer. + * @param width The width of the replacement. + */ + private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr, + /* Non Zero */ long paintPtr, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @FloatRange(from = 0) float width); + + private static native long nBuildMeasuredText( + /* Non Zero */ long nativeBuilderPtr, + @NonNull char[] text, + boolean computeHyphenation, + boolean computeLayout); + + private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr); + } +} |