Rendering the window frame with a second thread
Using a multi threaded render node to render the window frame
asynchronously from the application relayout.
Bug: 22527834
Bug: 24400680
Bug: 24459827
Bug: 24409773
Bug: 24537510
Change-Id: I1010fc6a8b6e38424178140afa3ca124433ab7e4
diff --git a/core/java/com/android/internal/widget/NonClientDecorView.java b/core/java/com/android/internal/widget/NonClientDecorView.java
index 56cf921..6960a3b 100644
--- a/core/java/com/android/internal/widget/NonClientDecorView.java
+++ b/core/java/com/android/internal/widget/NonClientDecorView.java
@@ -17,15 +17,23 @@
package com.android.internal.widget;
import android.content.Context;
+import android.graphics.Color;
import android.graphics.Rect;
+import android.os.Looper;
import android.os.RemoteException;
import android.util.AttributeSet;
+import android.view.Choreographer;
+import android.view.DisplayListCanvas;
import android.view.MotionEvent;
+import android.view.RenderNode;
+import android.view.ThreadedRenderer;
import android.view.View;
+import android.view.ViewRootImpl;
import android.widget.LinearLayout;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.Window;
+import android.view.WindowCallbacks;
import android.util.Log;
import android.util.TypedValue;
@@ -58,7 +66,7 @@
* This will be mitigated once b/22527834 will be addressed.
*/
public class NonClientDecorView extends LinearLayout
- implements View.OnClickListener, View.OnTouchListener {
+ implements View.OnClickListener, View.OnTouchListener, WindowCallbacks {
private final static String TAG = "NonClientDecorView";
// The height of a window which has focus in DIP.
private final int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
@@ -67,6 +75,8 @@
private PhoneWindow mOwner = null;
private boolean mWindowHasShadow = false;
private boolean mShowDecor = false;
+ // True when this object is listening for window size changes.
+ private boolean mAttachedCallbacksToRootViewImpl = false;
// True if the window is being dragged.
private boolean mDragging = false;
@@ -85,6 +95,9 @@
// to max until the first layout command has been executed.
private boolean mAllowUpdateElevation = false;
+ // The resize frame renderer.
+ private ResizeFrameThread mFrameRendererThread = null;
+
public NonClientDecorView(Context context) {
super(context);
}
@@ -108,6 +121,18 @@
// By changing the outline provider to BOUNDS, the window can remove its
// background without removing the shadow.
mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
+
+ if (!mAttachedCallbacksToRootViewImpl) {
+ // If there is no window callback installed there was no window set before. Set it now.
+ // Note that our ViewRootImpl object will not change.
+ getViewRootImpl().addWindowCallbacks(this);
+ mAttachedCallbacksToRootViewImpl = true;
+ } else if (mFrameRendererThread != null) {
+ // We are resizing and this call happened due to a configuration change. Tell the
+ // renderer about it.
+ mFrameRendererThread.onConfigurationChange();
+ }
+
findViewById(R.id.maximize_window).setOnClickListener(this);
findViewById(R.id.close_window).setOnClickListener(this);
}
@@ -251,7 +276,9 @@
**/
private void updateElevation() {
float elevation = 0;
- if (mWindowHasShadow) {
+ // Do not use a shadow when we are in resizing mode (mRenderer not null) since the shadow
+ // is bound to the content size and not the target size.
+ if (mWindowHasShadow && mFrameRendererThread == null) {
boolean fill = isFillingScreen();
elevation = fill ? 0 :
(mWindowHasFocus ? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP :
@@ -293,4 +320,270 @@
}
}
}
+
+ @Override
+ public void onWindowDragResizeStart(Rect initialBounds) {
+ if (mOwner.isDestroyed()) {
+ // If the owner's window is gone, we should not be able to come here anymore.
+ releaseResources();
+ return;
+ }
+ if (mFrameRendererThread != null) {
+ return;
+ }
+ final ThreadedRenderer renderer =
+ (ThreadedRenderer) mOwner.getDecorView().getHardwareRenderer();
+ if (renderer != null) {
+ mFrameRendererThread = new ResizeFrameThread(renderer, initialBounds);
+ // Get rid of the shadow while we are resizing. Shadow drawing takes considerable time.
+ // If we want to get the shadow shown while resizing, we would need to elevate a new
+ // element which owns the caption and has the elevation.
+ updateElevation();
+ }
+ }
+
+ @Override
+ public void onWindowDragResizeEnd() {
+ releaseThreadedRenderer();
+ }
+
+ @Override
+ public void onWindowSizeIsChanging(Rect newBounds) {
+ if (mFrameRendererThread != null) {
+ mFrameRendererThread.setTargetRect(newBounds);
+ }
+ }
+
+ /**
+ * Release the renderer thread which is usually done when the user stops resizing.
+ */
+ private void releaseThreadedRenderer() {
+ if (mFrameRendererThread != null) {
+ mFrameRendererThread.releaseRenderer();
+ mFrameRendererThread = null;
+ // Bring the shadow back.
+ updateElevation();
+ }
+ }
+
+ /**
+ * Called when the parent window is destroyed to release all resources. Note that this will also
+ * destroy the renderer thread.
+ */
+ private void releaseResources() {
+ releaseThreadedRenderer();
+ if (mAttachedCallbacksToRootViewImpl) {
+ ViewRootImpl.removeWindowCallbacks(this);
+ mAttachedCallbacksToRootViewImpl = false;
+ }
+ }
+
+ /**
+ * The thread which draws the chrome while we are resizing.
+ * It starts with the creation and it ends once someone calls destroy().
+ * Any size changes can be passed by a call to setTargetRect will passed to the thread and
+ * executed via the Choreographer.
+ */
+ private class ResizeFrameThread extends Thread implements Choreographer.FrameCallback {
+ // This is containing the last requested size by a resize command. Note that this size might
+ // or might not have been applied to the output already.
+ private final Rect mTargetRect = new Rect();
+
+ // The render nodes for the multi threaded renderer.
+ private ThreadedRenderer mRenderer;
+ private RenderNode mFrameNode;
+ private RenderNode mBackdropNode;
+
+ private final Rect mOldTargetRect = new Rect();
+ private final Rect mNewTargetRect = new Rect();
+ private Choreographer mChoreographer;
+
+ // Cached size values from the last render for the case that the view hierarchy is gone
+ // during a configuration change.
+ private int mLastContentWidth;
+ private int mLastContentHeight;
+ private int mLastCaptionHeight;
+ private int mLastXOffset;
+ private int mLastYOffset;
+
+ ResizeFrameThread(ThreadedRenderer renderer, Rect initialBounds) {
+ mRenderer = renderer;
+
+ // Create the render nodes for our frame and backdrop which can be resized independently
+ // from the content.
+ mFrameNode = RenderNode.create("FrameNode", null);
+ mBackdropNode = RenderNode.create("BackdropNode", null);
+
+ mRenderer.addRenderNode(mFrameNode, false);
+ mRenderer.addRenderNode(mBackdropNode, true);
+
+ // Set the initial bounds and draw once so that we do not get a broken frame.
+ mTargetRect.set(initialBounds);
+ changeWindowSize(initialBounds);
+
+ // Kick off our draw thread.
+ start();
+ }
+
+ /**
+ * Call this function asynchronously when the window size has been changed. The change will
+ * be picked up once per frame and the frame will be re-rendered accordingly.
+ * @param newTargetBounds The new target bounds.
+ */
+ public void setTargetRect(Rect newTargetBounds) {
+ synchronized (this) {
+ mTargetRect.set(newTargetBounds);
+ // Notify of a bounds change.
+ pingRenderLocked();
+ }
+ }
+
+ /**
+ * The window got replaced due to a configuration change.
+ */
+ public void onConfigurationChange() {
+ if (mRenderer != null) {
+ // Enforce a window redraw.
+ mOldTargetRect.set(0, 0, 0, 0);
+ pingRenderLocked();
+ }
+ }
+
+ /**
+ * All resources of the renderer will be released. This function can be called from the
+ * the UI thread as well as the renderer thread.
+ */
+ public void releaseRenderer() {
+ synchronized (this) {
+ if (mRenderer != null) {
+ // Invalidate the current content bounds.
+ mRenderer.setContentDrawBounds(0, 0, 0, 0);
+
+ // Remove the render nodes again (see comment above - better to do that only once).
+ mRenderer.removeRenderNode(mFrameNode);
+ mRenderer.removeRenderNode(mBackdropNode);
+
+ mRenderer = null;
+
+ // Exit the renderer loop.
+ pingRenderLocked();
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ Looper.prepare();
+ mChoreographer = Choreographer.getInstance();
+ Looper.loop();
+ } finally {
+ releaseRenderer();
+ }
+ synchronized (this) {
+ // Make sure no more messages are being sent.
+ mChoreographer = null;
+ }
+ }
+
+ /**
+ * The implementation of the FrameCallback.
+ * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
+ * in the {@link System#nanoTime()} timebase. Divide this value by {@code 1000000}
+ */
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ if (mRenderer == null) {
+ // Tell the looper to stop. We are done.
+ Looper.myLooper().quit();
+ return;
+ }
+ // Prevent someone from changing this while we are copying.
+ synchronized (this) {
+ mNewTargetRect.set(mTargetRect);
+ }
+ if (!mNewTargetRect.equals(mOldTargetRect)) {
+ mOldTargetRect.set(mNewTargetRect);
+ changeWindowSize(mNewTargetRect);
+ }
+ }
+
+ /**
+ * Resizing the frame to fit the new window size.
+ * @param newBounds The window bounds which needs to be drawn.
+ */
+ private void changeWindowSize(Rect newBounds) {
+ long startTime = System.currentTimeMillis();
+
+ // While a configuration change is taking place the view hierarchy might become
+ // inaccessible. For that case we remember the previous metrics to avoid flashes.
+ View caption = getChildAt(0);
+ View content = getChildAt(1);
+ if (content != null && caption != null) {
+ mLastContentWidth = content.getWidth();
+ mLastContentHeight = content.getHeight();
+ mLastCaptionHeight = caption.getHeight();
+
+ // Get the draw position within our surface.
+ int[] surfaceOrigin = new int[2];
+ surfaceOrigin[0] = 0;
+ surfaceOrigin[1] = 0;
+
+ // Get the shadow offsets.
+ getLocationInSurface(surfaceOrigin);
+ mLastXOffset = surfaceOrigin[0];
+ mLastYOffset = surfaceOrigin[1];
+ }
+
+ // Since the surface is spanning the entire screen, we have to add the start offset of
+ // the bounds to get to the surface location.
+ final int left = mLastXOffset + newBounds.left;
+ final int top = mLastYOffset + newBounds.top;
+ final int width = newBounds.width();
+ final int height = newBounds.height();
+
+ // Produce the draw calls.
+ // TODO(skuhne): Create a separate caption view which draws this. If the shadow should
+ // be resized while the window resizes, this hierarchy needs to have the elevation.
+ // That said - it is probably no good idea to draw the shadow every time since it costs
+ // a considerable time which we should rather spend for resizing the content and it does
+ // barely show while the entire screen is moving.
+ mFrameNode.setLeftTopRightBottom(left, top, left + width, top + mLastCaptionHeight);
+ DisplayListCanvas canvas = mFrameNode.start(width, height);
+ canvas.drawColor(Color.BLACK);
+ mFrameNode.end(canvas);
+
+ mBackdropNode.setLeftTopRightBottom(left, top + mLastCaptionHeight, left + width,
+ top + height);
+
+ // The backdrop: clear everything with the background. Clipping is done elsewhere.
+ canvas = mBackdropNode.start(width, height - mLastCaptionHeight);
+ // TODO(skuhne): mOwner.getDecorView().mBackgroundFallback.draw(..) - or similar.
+ // Note: This might not work (calculator for example uses a transparent background).
+ canvas.drawColor(0xff808080);
+ mBackdropNode.end(canvas);
+
+ // The current content buffer is drawn here.
+ mRenderer.setContentDrawBounds(
+ mLastXOffset,
+ mLastYOffset + mLastCaptionHeight,
+ mLastXOffset + mLastContentWidth,
+ mLastYOffset + mLastCaptionHeight + mLastContentHeight);
+
+ // We need to render both rendered nodes explicitly.
+ mRenderer.drawRenderNode(mFrameNode);
+ mRenderer.drawRenderNode(mBackdropNode);
+ }
+
+ /**
+ * Sends a message to the renderer to wake up and perform the next action which can be
+ * either the next rendering or the self destruction if mRenderer is null.
+ * Note: This call must be synchronized.
+ */
+ private void pingRenderLocked() {
+ if (mChoreographer != null) {
+ mChoreographer.postFrameCallback(this);
+ }
+ }
+ }
}