diff --git a/apct-tests/perftests/core/src/android/graphics/perftests/RenderNodePerfTest.java b/apct-tests/perftests/core/src/android/graphics/perftests/RenderNodePerfTest.java index d18aa51bfcd38..62dd124beb861 100644 --- a/apct-tests/perftests/core/src/android/graphics/perftests/RenderNodePerfTest.java +++ b/apct-tests/perftests/core/src/android/graphics/perftests/RenderNodePerfTest.java @@ -17,7 +17,6 @@ package android.graphics.perftests; import android.graphics.Outline; -import android.graphics.RecordingCanvas; import android.graphics.RenderNode; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; @@ -62,7 +61,7 @@ public class RenderNodePerfTest { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); RenderNode node = RenderNode.create("LinearLayout", null); while (state.keepRunning()) { - node.isValid(); + node.hasDisplayList(); } } @@ -71,8 +70,8 @@ public class RenderNodePerfTest { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); RenderNode node = RenderNode.create("LinearLayout", null); while (state.keepRunning()) { - RecordingCanvas canvas = node.start(100, 100); - node.end(canvas); + node.startRecording(100, 100); + node.endRecording(); } } @@ -80,17 +79,16 @@ public class RenderNodePerfTest { public void testStartEndDeepHierarchy() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); RenderNode[] nodes = new RenderNode[30]; - RecordingCanvas[] canvases = new RecordingCanvas[nodes.length]; for (int i = 0; i < nodes.length; i++) { nodes[i] = RenderNode.create("LinearLayout", null); } while (state.keepRunning()) { for (int i = 0; i < nodes.length; i++) { - canvases[i] = nodes[i].start(100, 100); + nodes[i].startRecording(100, 100); } for (int i = nodes.length - 1; i >= 0; i--) { - nodes[i].end(canvases[i]); + nodes[i].endRecording(); } } } diff --git a/apct-tests/perftests/core/src/android/view/ViewShowHidePerfTest.java b/apct-tests/perftests/core/src/android/view/ViewShowHidePerfTest.java index 6159da4fc3f5d..dc4d4bd782fb0 100644 --- a/apct-tests/perftests/core/src/android/view/ViewShowHidePerfTest.java +++ b/apct-tests/perftests/core/src/android/view/ViewShowHidePerfTest.java @@ -155,7 +155,7 @@ public class ViewShowHidePerfTest { } private void updateAndValidateDisplayList(View view) { - boolean hasDisplayList = view.updateDisplayListIfDirty().isValid(); + boolean hasDisplayList = view.updateDisplayListIfDirty().hasDisplayList(); assertTrue(hasDisplayList); } diff --git a/core/java/android/view/GhostView.java b/core/java/android/view/GhostView.java index 98ed21735e626..74c801b1dc5bd 100644 --- a/core/java/android/view/GhostView.java +++ b/core/java/android/view/GhostView.java @@ -52,7 +52,7 @@ public class GhostView extends View { RecordingCanvas dlCanvas = (RecordingCanvas) canvas; mView.mRecreateDisplayList = true; RenderNode renderNode = mView.updateDisplayListIfDirty(); - if (renderNode.isValid()) { + if (renderNode.hasDisplayList()) { dlCanvas.insertReorderBarrier(); // enable shadow for this rendernode dlCanvas.drawRenderNode(renderNode); dlCanvas.insertInorderBarrier(); // re-disable reordering/shadows diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index c1ab4d4b895e8..f0f4c1c595b80 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -694,7 +694,7 @@ public final class ThreadedRenderer { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()"); updateViewTreeDisplayList(view); - if (mRootNodeNeedsUpdate || !mRootNode.isValid()) { + if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) { RecordingCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight); try { final int saveCount = canvas.save(); @@ -857,7 +857,7 @@ public final class ThreadedRenderer { void buildLayer(RenderNode node) { - if (node.isValid()) { + if (node.hasDisplayList()) { nBuildLayer(mNativeProxy, node.mNativeRenderNode); } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1157b287e5500..1493cd7f2eda2 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -16658,7 +16658,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @UnsupportedAppUsage void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) { if (!isHardwareAccelerated() - || !mRenderNode.isValid() + || !mRenderNode.hasDisplayList() || (mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) { if (invalidateParent) { invalidateParentCaches(); @@ -19047,7 +19047,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, switch (mLayerType) { case LAYER_TYPE_HARDWARE: updateDisplayListIfDirty(); - if (attachInfo.mThreadedRenderer != null && mRenderNode.isValid()) { + if (attachInfo.mThreadedRenderer != null && mRenderNode.hasDisplayList()) { attachInfo.mThreadedRenderer.buildLayer(mRenderNode); } break; @@ -19214,11 +19214,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 - || !renderNode.isValid() + || !renderNode.hasDisplayList() || (mRecreateDisplayList)) { // Don't need to recreate the display list, just need to tell our // children to restore/recreate theirs - if (renderNode.isValid() + if (renderNode.hasDisplayList() && !mRecreateDisplayList) { mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; @@ -19235,7 +19235,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int height = mBottom - mTop; int layerType = getLayerType(); - final RecordingCanvas canvas = renderNode.start(width, height); + final RecordingCanvas canvas = renderNode.startRecording(width, height); try { if (layerType == LAYER_TYPE_SOFTWARE) { @@ -19266,7 +19266,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } } finally { - renderNode.end(canvas); + renderNode.endRecording(); setDisplayListProperties(renderNode); } } else { @@ -20118,7 +20118,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Delay getting the display list until animation-driven alpha values are // set up and possibly passed on to the view renderNode = updateDisplayListIfDirty(); - if (!renderNode.isValid()) { + if (!renderNode.hasDisplayList()) { // Uncommon, but possible. If a view is removed from the hierarchy during the call // to getDisplayList(), the display list will be marked invalid and we should not // try to use it again. @@ -20581,7 +20581,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode); final RenderNode renderNode = mBackgroundRenderNode; - if (renderNode != null && renderNode.isValid()) { + if (renderNode != null && renderNode.hasDisplayList()) { setBackgroundRenderNodeProperties(renderNode); ((RecordingCanvas) canvas).drawRenderNode(renderNode); return; @@ -20635,7 +20635,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final Rect bounds = drawable.getBounds(); final int width = bounds.width(); final int height = bounds.height(); - final RecordingCanvas canvas = renderNode.start(width, height); + final RecordingCanvas canvas = renderNode.startRecording(width, height); // Reverse left/top translation done by drawable canvas, which will // instead be applied by rendernode's LTRB bounds below. This way, the @@ -20646,7 +20646,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, try { drawable.draw(canvas); } finally { - renderNode.end(canvas); + renderNode.endRecording(); } // Set up drawable properties that are view-independent. diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 48c164f726bcc..feef853c9b199 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -257,7 +257,7 @@ public class Editor { needsToBeShifted = true; } boolean needsRecord() { - return isDirty || !renderNode.isValid(); + return isDirty || !renderNode.hasDisplayList(); } } private TextRenderNode[] mTextRenderNodes; @@ -542,7 +542,7 @@ public class Editor { for (int i = 0; i < mTextRenderNodes.length; i++) { RenderNode displayList = mTextRenderNodes[i] != null ? mTextRenderNodes[i].renderNode : null; - if (displayList != null && displayList.isValid()) { + if (displayList != null && displayList.hasDisplayList()) { displayList.discardDisplayList(); } } diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 632edfa4e46a1..e178ab4ba0167 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -1295,13 +1295,13 @@ public final class Bitmap implements Parcelable { node.setLeftTopRightBottom(0, 0, width, height); node.setClipToBounds(false); node.setForceDarkAllowed(false); - final RecordingCanvas canvas = node.start(width, height); + 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); diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java index b61488cba9342..320fb20e5b980 100644 --- a/graphics/java/android/graphics/RenderNode.java +++ b/graphics/java/android/graphics/RenderNode.java @@ -19,7 +19,6 @@ package android.graphics; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.UnsupportedAppUsage; import android.view.NativeVectorDrawableAnimator; import android.view.RenderNodeAnimator; import android.view.View; @@ -33,89 +32,92 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - *
A display list records a series of graphics related operations and can replay - * them later. Display lists are usually built by recording operations on a - * {@link RecordingCanvas}. Replaying the operations from a display list avoids - * executing application code on every frame, and is thus much more efficient.
+ *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.
* - *Display lists are used internally for all views by default, and are not - * typically used directly. One reason to consider using a display is a custom - * {@link View} implementation that needs to issue a large number of drawing commands. - * When the view invalidates, all the drawing commands must be reissued, even if - * large portions of the drawing command stream stay the same frame to frame, which - * can become a performance bottleneck. To solve this issue, a custom View might split - * its content into several display lists. A display list is updated only when its - * content, and only its content, needs to be updated.
+ *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.
* - *A text editor might for instance store each paragraph into its own display list. + *
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.
* *Display lists can only be replayed using a {@link RecordingCanvas}. They are not + *
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()}.
* - *
- * ThreadedRenderer renderer = myView.getThreadedRenderer();
- * if (renderer != null) {
- * DisplayList displayList = renderer.createDisplayList();
- * RecordingCanvas canvas = displayList.start(width, height);
- * try {
- * // Draw onto the canvas
- * // For instance: canvas.drawBitmap(...);
- * } finally {
- * displayList.end();
- * }
+ * 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();
* }
*
*
- *
* protected void onDraw(Canvas canvas) {
- * if (canvas.isHardwareAccelerated()) {
- * RecordingCanvas displayListCanvas = (RecordingCanvas) canvas;
- * displayListCanvas.drawDisplayList(mDisplayList);
+ * 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);
* }
* }
*
*
* This step is not mandatory but recommended if you want to release resources - * held by a display list as soon as possible.
+ * held by a display list as soon as possible. Most significantly any bitmaps it may contain. *- * // Mark this display list invalid, it cannot be used for drawing anymore, - * // and release resources held by this display list - * displayList.clear(); + * // Discards the display list content allowing for any held resources to be released. + * // After calling this + * renderNode.discardDisplayList(); ** + * *
In addition, a display list offers several properties, such as + *
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
- * drawBitmap() calls.
canvas.drawBitmap() calls.
*
*
* private void createDisplayList() {
- * mDisplayList = DisplayList.create("MyDisplayList");
- * RecordingCanvas canvas = mDisplayList.start(width, height);
+ * 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 {
- * displayList.end();
+ * mRenderNode.endRecording();
* }
* }
*
* protected void onDraw(Canvas canvas) {
- * if (canvas.isHardwareAccelerated()) {
- * RecordingCanvas displayListCanvas = (RecordingCanvas) canvas;
- * displayListCanvas.drawDisplayList(mDisplayList);
+ * if (canvas instanceof RecordingCanvas) {
+ * RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
+ * recordingCanvas.drawRenderNode(mRenderNode);
* }
* }
*
@@ -124,13 +126,16 @@ import java.lang.annotation.RetentionPolicy;
* // 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
- * mDisplayList.offsetLeftAndRight(x);
+ * mRenderNode.offsetLeftAndRight(x);
* invalidate();
* }
*
*
* Display lists must be created on and manipulated from the UI thread only.
+ *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.
* * @hide */ @@ -147,6 +152,7 @@ public class RenderNode { */ public final long mNativeRenderNode; private final AnimationHost mAnimationHost; + private RecordingCanvas mCurrentRecordingCanvas; private RenderNode(String name, AnimationHost animationHost) { mNativeRenderNode = nCreate(name); @@ -171,7 +177,6 @@ public class RenderNode { * * @return A new RenderNode. */ - @UnsupportedAppUsage public static RenderNode create(String name, @Nullable AnimationHost animationHost) { return new RenderNode(name, animationHost); } @@ -226,64 +231,106 @@ public class RenderNode { * operations performed on the returned canvas are recorded and * stored in this display list. * - * Calling this method will mark the render node invalid until - * {@link #end(RecordingCanvas)} is called. - * Only valid render nodes can be replayed. + * {@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 - * @param height The height of the recording viewport + * @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 #end(RecordingCanvas) - * @see #isValid() + * @see #endRecording() + * @see #hasDisplayList() */ - @UnsupportedAppUsage - public RecordingCanvas start(int width, int height) { - return RecordingCanvas.obtain(this, width, height); + 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 #start(int, int)} but with the RenderNode's width & height + * 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 start() { + public RecordingCanvas startRecording() { return RecordingCanvas.obtain(this, nGetWidth(mNativeRenderNode), nGetHeight(mNativeRenderNode)); } /** - * Ends the recording for this display list. A display list cannot be - * replayed if recording is not finished. Calling this method marks - * the display list valid and {@link #isValid()} will return true. - * - * @see #start(int, int) - * @see #isValid() + * @deprecated use {@link #startRecording(int, int)} instead + * @hide */ - @UnsupportedAppUsage - public void end(RecordingCanvas canvas) { + @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(); } + /** + * @deprecated use {@link #endRecording()} instead + * @hide + */ + @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. */ - @UnsupportedAppUsage public void discardDisplayList() { nSetDisplayList(mNativeRenderNode, 0); } /** - * Returns whether the RenderNode's display list content is currently usable. - * If this returns false, the display list should be re-recorded prior to replaying it. + * Returns whether the RenderNode has a display list. If this returns false, the RenderNode + * should be re-recorded with {@link #startRecording()} and {@link #endRecording()}. * - * @return boolean true if the display list is able to be replayed, false otherwise. + * 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. */ - @UnsupportedAppUsage - public boolean isValid() { + public boolean hasDisplayList() { return nIsValid(mNativeRenderNode); } @@ -358,7 +405,6 @@ public class RenderNode { * * @param clipToBounds true if the display list should clip to its bounds */ - @UnsupportedAppUsage public boolean setClipToBounds(boolean clipToBounds) { return nSetClipToBounds(mNativeRenderNode, clipToBounds); } @@ -370,7 +416,6 @@ public class RenderNode { * @param shouldProject true if the display list should be projected onto a * containing volume. */ - @UnsupportedAppUsage public boolean setProjectBackwards(boolean shouldProject) { return nSetProjectBackwards(mNativeRenderNode, shouldProject); } @@ -521,7 +566,6 @@ public class RenderNode { * @see android.view.View#hasOverlappingRendering() * @see #hasOverlappingRendering() */ - @UnsupportedAppUsage public boolean setHasOverlappingRendering(boolean hasOverlappingRendering) { return nSetHasOverlappingRendering(mNativeRenderNode, hasOverlappingRendering); } @@ -872,7 +916,6 @@ public class RenderNode { * @see View#setRight(int) * @see View#setBottom(int) */ - @UnsupportedAppUsage public boolean setLeftTopRightBottom(int left, int top, int right, int bottom) { return nSetLeftTopRightBottom(mNativeRenderNode, left, top, right, bottom); } @@ -885,7 +928,6 @@ public class RenderNode { * * @see View#offsetLeftAndRight(int) */ - @UnsupportedAppUsage public boolean offsetLeftAndRight(int offset) { return nOffsetLeftAndRight(mNativeRenderNode, offset); } @@ -906,7 +948,6 @@ public class RenderNode { * Outputs the display list to the log. This method exists for use by * tools to output display lists for selected nodes to the log. */ - @UnsupportedAppUsage public void output() { nOutput(mNativeRenderNode); }