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/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index dcef142..304e9c0 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -349,7 +349,7 @@
* @param right The right side of the protected bounds.
* @param bottom The bottom side of the protected bounds.
*/
- public void setContentOverdrawProtectionBounds(int left, int top, int right, int bottom) {
+ public void setContentDrawBounds(int left, int top, int right, int bottom) {
mStagedContentBounds.set(left, top, right, bottom);
}
@@ -370,9 +370,9 @@
// renderer.
if (!mCurrentContentBounds.equals(mStagedContentBounds)) {
mCurrentContentBounds.set(mStagedContentBounds);
- nSetContentOverdrawProtectionBounds(mNativeProxy, mCurrentContentBounds.left,
- mCurrentContentBounds.top, mCurrentContentBounds.right,
- mCurrentContentBounds.bottom);
+ nSetContentDrawBounds(mNativeProxy, mCurrentContentBounds.left,
+ mCurrentContentBounds.top, mCurrentContentBounds.right,
+ mCurrentContentBounds.bottom);
}
attachInfo.mIgnoreDirtyState = false;
@@ -600,6 +600,6 @@
boolean placeFront);
private static native void nRemoveRenderNode(long nativeProxy, long rootRenderNode);
private static native void nDrawRenderNode(long nativeProxy, long rootRenderNode);
- private static native void nSetContentOverdrawProtectionBounds(long nativeProxy, int left,
+ private static native void nSetContentDrawBounds(long nativeProxy, int left,
int top, int right, int bottom);
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index f6c60ed..7cf23e7 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -73,6 +73,7 @@
import android.view.animation.Interpolator;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
+import android.view.WindowCallbacks;
import android.widget.Scroller;
import com.android.internal.R;
@@ -115,6 +116,12 @@
private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV;
/**
+ * Set to false if we do not want to use the multi threaded renderer. Note that by disabling
+ * this, WindowCallbacks will not fire.
+ */
+ private static final boolean USE_MT_RENDERER = true;
+
+ /**
* Set this system property to true to force the view hierarchy to render
* at 60 Hz. This can be used to measure the potential framerate.
*/
@@ -132,11 +139,11 @@
static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();
- static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList<Runnable>();
+ static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList();
static boolean sFirstDrawComplete = false;
+ static final ArrayList<WindowCallbacks> sWindowCallbacks = new ArrayList();
- static final ArrayList<ComponentCallbacks> sConfigCallbacks
- = new ArrayList<ComponentCallbacks>();
+ static final ArrayList<ComponentCallbacks> sConfigCallbacks = new ArrayList();
final Context mContext;
final IWindowSession mWindowSession;
@@ -417,6 +424,22 @@
}
}
+ public static void addWindowCallbacks(WindowCallbacks callback) {
+ if (USE_MT_RENDERER) {
+ synchronized (sWindowCallbacks) {
+ sWindowCallbacks.add(callback);
+ }
+ }
+ }
+
+ public static void removeWindowCallbacks(WindowCallbacks callback) {
+ if (USE_MT_RENDERER) {
+ synchronized (sWindowCallbacks) {
+ sWindowCallbacks.remove(callback);
+ }
+ }
+ }
+
// FIXME for perf testing only
private boolean mProfile = false;
@@ -1378,6 +1401,7 @@
mAttachInfo.mWindowVisibility = viewVisibility;
host.dispatchWindowVisibilityChanged(viewVisibility);
if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
+ endDragResizing();
destroyHardwareResources();
}
if (viewVisibility == View.GONE) {
@@ -1701,14 +1725,20 @@
final boolean dragResizing = (relayoutResult
& WindowManagerGlobal.RELAYOUT_RES_DRAG_RESIZING) != 0;
if (mDragResizing != dragResizing) {
- mDragResizing = dragResizing;
- mFullRedrawNeeded = true;
+ if (dragResizing) {
+ startDragResizing(frame);
+ } else {
+ // We shouldn't come here, but if we come we should end the resize.
+ endDragResizing();
+ }
}
- if (dragResizing) {
- mCanvasOffsetX = mWinFrame.left;
- mCanvasOffsetY = mWinFrame.top;
- } else {
- mCanvasOffsetX = mCanvasOffsetY = 0;
+ if (!USE_MT_RENDERER) {
+ if (dragResizing) {
+ mCanvasOffsetX = mWinFrame.left;
+ mCanvasOffsetY = mWinFrame.top;
+ } else {
+ mCanvasOffsetX = mCanvasOffsetY = 0;
+ }
}
} catch (RemoteException e) {
}
@@ -6635,6 +6665,15 @@
Configuration newConfig) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
+ // Tell all listeners that we are resizing the window so that the chrome can get
+ // updated as fast as possible on a separate thread,
+ if (mViewAncestor.get().mDragResizing) {
+ synchronized (sWindowCallbacks) {
+ for (int i = sWindowCallbacks.size() - 1; i >= 0; i--) {
+ sWindowCallbacks.get(i).onWindowSizeIsChanging(frame);
+ }
+ }
+ }
viewAncestor.dispatchResized(frame, overscanInsets, contentInsets,
visibleInsets, stableInsets, outsets, reportDraw, newConfig);
}
@@ -6804,6 +6843,36 @@
}
/**
+ * Start a drag resizing which will inform all listeners that a window resize is taking place.
+ */
+ private void startDragResizing(Rect initialBounds) {
+ if (!mDragResizing) {
+ mDragResizing = true;
+ synchronized (sWindowCallbacks) {
+ for (int i = sWindowCallbacks.size() - 1; i >= 0; i--) {
+ sWindowCallbacks.get(i).onWindowDragResizeStart(initialBounds);
+ }
+ }
+ mFullRedrawNeeded = true;
+ }
+ }
+
+ /**
+ * End a drag resize which will inform all listeners that a window resize has ended.
+ */
+ private void endDragResizing() {
+ if (mDragResizing) {
+ mDragResizing = false;
+ synchronized (sWindowCallbacks) {
+ for (int i = sWindowCallbacks.size() - 1; i >= 0; i--) {
+ sWindowCallbacks.get(i).onWindowDragResizeEnd();
+ }
+ }
+ mFullRedrawNeeded = true;
+ }
+ }
+
+ /**
* Class for managing the accessibility interaction connection
* based on the global accessibility state.
*/
diff --git a/core/java/android/view/WindowCallbacks.java b/core/java/android/view/WindowCallbacks.java
new file mode 100644
index 0000000..cb6e983
--- /dev/null
+++ b/core/java/android/view/WindowCallbacks.java
@@ -0,0 +1,48 @@
+/*
+ * 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.view;
+
+import android.graphics.Rect;
+
+/**
+ * These callbacks are used to communicate window configuration changes while the user is performing
+ * window changes.
+ * @hide
+ */
+public interface WindowCallbacks {
+ /**
+ * Called by the system when the window got changed by the user, before the layouter got called.
+ * It can be used to perform a "quick and dirty" resize which should never take more then 4ms to
+ * complete.
+ *
+ * <p>At the time the layouting has not happened yet.
+ *
+ * @param newBounds The new window frame bounds.
+ */
+ void onWindowSizeIsChanging(Rect newBounds);
+
+ /**
+ * Called when a drag resize starts.
+ * @param initialBounds The initial bounds where the window will be.
+ */
+ void onWindowDragResizeStart(Rect initialBounds);
+
+ /**
+ * Called when a drag resize ends.
+ */
+ void onWindowDragResizeEnd();
+}
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index b6d7364..c9b8119 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -2615,7 +2615,7 @@
if (action == MotionEvent.ACTION_DOWN) {
int y = (int)event.getY();
if (y >= (getHeight()-5) && !mWindow.hasChildren()) {
- Log.i(TAG, "Watchiing!");
+ Log.i(TAG, "Watching!");
mWatchingForMenu = true;
}
return false;
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);
+ }
+ }
+ }
}