blob: 1abb59b006dd97382faabbeb7b7154e103495df7 [file] [log] [blame]
/*
* 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 com.android.internal.policy;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.view.Choreographer;
import android.view.DisplayListCanvas;
import android.view.RenderNode;
import android.view.ThreadedRenderer;
/**
* The thread which draws a fill in background while the app is resizing in areas where the app
* content draw is lagging behind the resize operation.
* 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.
* @hide
*/
public class BackdropFrameRenderer extends Thread implements Choreographer.FrameCallback {
private DecorView mDecorView;
// 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 mFrameAndBackdropNode;
private RenderNode mSystemBarBackgroundNode;
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;
// Whether to report when next frame is drawn or not.
private boolean mReportNextDraw;
private Drawable mCaptionBackgroundDrawable;
private Drawable mUserCaptionBackgroundDrawable;
private Drawable mResizingBackgroundDrawable;
private ColorDrawable mStatusBarColor;
private ColorDrawable mNavigationBarColor;
private boolean mOldFullscreen;
private boolean mFullscreen;
private final int mResizeMode;
private final Rect mOldSystemInsets = new Rect();
private final Rect mOldStableInsets = new Rect();
private final Rect mSystemInsets = new Rect();
private final Rect mStableInsets = new Rect();
public BackdropFrameRenderer(DecorView decorView, ThreadedRenderer renderer, Rect initialBounds,
Drawable resizingBackgroundDrawable, Drawable captionBackgroundDrawable,
Drawable userCaptionBackgroundDrawable, int statusBarColor, int navigationBarColor,
boolean fullscreen, Rect systemInsets, Rect stableInsets, int resizeMode) {
setName("ResizeFrame");
mRenderer = renderer;
onResourcesLoaded(decorView, resizingBackgroundDrawable, captionBackgroundDrawable,
userCaptionBackgroundDrawable, statusBarColor, navigationBarColor);
// Create a render node for the content and frame backdrop
// which can be resized independently from the content.
mFrameAndBackdropNode = RenderNode.create("FrameAndBackdropNode", null);
mRenderer.addRenderNode(mFrameAndBackdropNode, true);
// Set the initial bounds and draw once so that we do not get a broken frame.
mTargetRect.set(initialBounds);
mFullscreen = fullscreen;
mOldFullscreen = fullscreen;
mSystemInsets.set(systemInsets);
mStableInsets.set(stableInsets);
mOldSystemInsets.set(systemInsets);
mOldStableInsets.set(stableInsets);
mResizeMode = resizeMode;
// Kick off our draw thread.
start();
}
void onResourcesLoaded(DecorView decorView, Drawable resizingBackgroundDrawable,
Drawable captionBackgroundDrawableDrawable, Drawable userCaptionBackgroundDrawable,
int statusBarColor, int navigationBarColor) {
mDecorView = decorView;
mResizingBackgroundDrawable = resizingBackgroundDrawable != null
&& resizingBackgroundDrawable.getConstantState() != null
? resizingBackgroundDrawable.getConstantState().newDrawable()
: null;
mCaptionBackgroundDrawable = captionBackgroundDrawableDrawable != null
&& captionBackgroundDrawableDrawable.getConstantState() != null
? captionBackgroundDrawableDrawable.getConstantState().newDrawable()
: null;
mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable != null
&& userCaptionBackgroundDrawable.getConstantState() != null
? userCaptionBackgroundDrawable.getConstantState().newDrawable()
: null;
if (mCaptionBackgroundDrawable == null) {
mCaptionBackgroundDrawable = mResizingBackgroundDrawable;
}
if (statusBarColor != 0) {
mStatusBarColor = new ColorDrawable(statusBarColor);
addSystemBarNodeIfNeeded();
} else {
mStatusBarColor = null;
}
if (navigationBarColor != 0) {
mNavigationBarColor = new ColorDrawable(navigationBarColor);
addSystemBarNodeIfNeeded();
} else {
mNavigationBarColor = null;
}
}
private void addSystemBarNodeIfNeeded() {
if (mSystemBarBackgroundNode != null) {
return;
}
mSystemBarBackgroundNode = RenderNode.create("SystemBarBackgroundNode", null);
mRenderer.addRenderNode(mSystemBarBackgroundNode, false);
}
/**
* Call this function asynchronously when the window size has been changed or when the insets
* have changed or whether window switched between a fullscreen or non-fullscreen layout.
* The change will be picked up once per frame and the frame will be re-rendered accordingly.
*
* @param newTargetBounds The new target bounds.
* @param fullscreen Whether the window is currently drawing in fullscreen.
* @param systemInsets The current visible system insets for the window.
* @param stableInsets The stable insets for the window.
*/
public void setTargetRect(Rect newTargetBounds, boolean fullscreen, Rect systemInsets,
Rect stableInsets) {
synchronized (this) {
mFullscreen = fullscreen;
mTargetRect.set(newTargetBounds);
mSystemInsets.set(systemInsets);
mStableInsets.set(stableInsets);
// Notify of a bounds change.
pingRenderLocked(false /* drawImmediate */);
}
}
/**
* The window got replaced due to a configuration change.
*/
public void onConfigurationChange() {
synchronized (this) {
if (mRenderer != null) {
// Enforce a window redraw.
mOldTargetRect.set(0, 0, 0, 0);
pingRenderLocked(false /* drawImmediate */);
}
}
}
/**
* 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 node again
// (see comment above - better to do that only once).
mRenderer.removeRenderNode(mFrameAndBackdropNode);
if (mSystemBarBackgroundNode != null) {
mRenderer.removeRenderNode(mSystemBarBackgroundNode);
}
mRenderer = null;
// Exit the renderer loop.
pingRenderLocked(false /* drawImmediate */);
}
}
}
@Override
public void run() {
try {
Looper.prepare();
synchronized (this) {
mChoreographer = Choreographer.getInstance();
}
Looper.loop();
} finally {
releaseRenderer();
}
synchronized (this) {
// Make sure no more messages are being sent.
mChoreographer = null;
Choreographer.releaseInstance();
}
}
/**
* 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) {
synchronized (this) {
if (mRenderer == null) {
reportDrawIfNeeded();
// Tell the looper to stop. We are done.
Looper.myLooper().quit();
return;
}
doFrameUncheckedLocked();
}
}
private void doFrameUncheckedLocked() {
mNewTargetRect.set(mTargetRect);
if (!mNewTargetRect.equals(mOldTargetRect)
|| mOldFullscreen != mFullscreen
|| !mStableInsets.equals(mOldStableInsets)
|| !mSystemInsets.equals(mOldSystemInsets)
|| mReportNextDraw) {
mOldFullscreen = mFullscreen;
mOldTargetRect.set(mNewTargetRect);
mOldSystemInsets.set(mSystemInsets);
mOldStableInsets.set(mStableInsets);
redrawLocked(mNewTargetRect, mFullscreen, mSystemInsets, mStableInsets);
}
}
/**
* The content is about to be drawn and we got the location of where it will be shown.
* If a "redrawLocked" call has already been processed, we will re-issue the call
* if the previous call was ignored since the size was unknown.
* @param xOffset The x offset where the content is drawn to.
* @param yOffset The y offset where the content is drawn to.
* @param xSize The width size of the content. This should not be 0.
* @param ySize The height of the content.
* @return true if a frame should be requested after the content is drawn; false otherwise.
*/
public boolean onContentDrawn(int xOffset, int yOffset, int xSize, int ySize) {
synchronized (this) {
final boolean firstCall = mLastContentWidth == 0;
// The current content buffer is drawn here.
mLastContentWidth = xSize;
mLastContentHeight = ySize - mLastCaptionHeight;
mLastXOffset = xOffset;
mLastYOffset = yOffset;
// Inform the renderer of the content's new bounds
mRenderer.setContentDrawBounds(
mLastXOffset,
mLastYOffset,
mLastXOffset + mLastContentWidth,
mLastYOffset + mLastCaptionHeight + mLastContentHeight);
// If this was the first call and redrawLocked got already called prior
// to us, we should re-issue a redrawLocked now.
return firstCall
&& (mLastCaptionHeight != 0 || !mDecorView.isShowingCaption());
}
}
public void onRequestDraw(boolean reportNextDraw) {
synchronized (this) {
mReportNextDraw = reportNextDraw;
mOldTargetRect.set(0, 0, 0, 0);
pingRenderLocked(true /* drawImmediate */);
}
}
/**
* Redraws the background, the caption and the system inset backgrounds if something changed.
*
* @param newBounds The window bounds which needs to be drawn.
* @param fullscreen Whether the window is currently drawing in fullscreen.
* @param systemInsets The current visible system insets for the window.
* @param stableInsets The stable insets for the window.
*/
private void redrawLocked(Rect newBounds, boolean fullscreen, Rect systemInsets,
Rect stableInsets) {
// While a configuration change is taking place the view hierarchy might become
// inaccessible. For that case we remember the previous metrics to avoid flashes.
// Note that even when there is no visible caption, the caption child will exist.
final int captionHeight = mDecorView.getCaptionHeight();
// The caption height will probably never dynamically change while we are resizing.
// Once set to something other then 0 it should be kept that way.
if (captionHeight != 0) {
// Remember the height of the caption.
mLastCaptionHeight = captionHeight;
}
// Make sure that the other thread has already prepared the render draw calls for the
// content. If any size is 0, we have to wait for it to be drawn first.
if ((mLastCaptionHeight == 0 && mDecorView.isShowingCaption()) ||
mLastContentWidth == 0 || mLastContentHeight == 0) {
return;
}
// 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();
mFrameAndBackdropNode.setLeftTopRightBottom(left, top, left + width, top + height);
// Draw the caption and content backdrops in to our render node.
DisplayListCanvas canvas = mFrameAndBackdropNode.start(width, height);
final Drawable drawable = mUserCaptionBackgroundDrawable != null
? mUserCaptionBackgroundDrawable : mCaptionBackgroundDrawable;
if (drawable != null) {
drawable.setBounds(0, 0, left + width, top + mLastCaptionHeight);
drawable.draw(canvas);
}
// The backdrop: clear everything with the background. Clipping is done elsewhere.
if (mResizingBackgroundDrawable != null) {
mResizingBackgroundDrawable.setBounds(0, mLastCaptionHeight, left + width, top + height);
mResizingBackgroundDrawable.draw(canvas);
}
mFrameAndBackdropNode.end(canvas);
drawColorViews(left, top, width, height, fullscreen, systemInsets, stableInsets);
// We need to render the node explicitly
mRenderer.drawRenderNode(mFrameAndBackdropNode);
reportDrawIfNeeded();
}
private void drawColorViews(int left, int top, int width, int height,
boolean fullscreen, Rect systemInsets, Rect stableInsets) {
if (mSystemBarBackgroundNode == null) {
return;
}
DisplayListCanvas canvas = mSystemBarBackgroundNode.start(width, height);
mSystemBarBackgroundNode.setLeftTopRightBottom(left, top, left + width, top + height);
final int topInset = DecorView.getColorViewTopInset(mStableInsets.top, mSystemInsets.top);
final int bottomInset = DecorView.getColorViewBottomInset(stableInsets.bottom,
systemInsets.bottom);
final int rightInset = DecorView.getColorViewRightInset(stableInsets.right,
systemInsets.right);
final int leftInset = DecorView.getColorViewLeftInset(stableInsets.left,
systemInsets.left);
if (mStatusBarColor != null) {
mStatusBarColor.setBounds(0, 0, left + width, topInset);
mStatusBarColor.draw(canvas);
}
// We only want to draw the navigation bar if our window is currently fullscreen because we
// don't want the navigation bar background be moving around when resizing in docked mode.
// However, we need it for the transitions into/out of docked mode.
if (mNavigationBarColor != null && fullscreen) {
final int size = DecorView.getNavBarSize(bottomInset, rightInset, leftInset);
if (DecorView.isNavBarToRightEdge(bottomInset, rightInset)) {
mNavigationBarColor.setBounds(width - size, 0, width, height);
} else if (DecorView.isNavBarToLeftEdge(bottomInset, leftInset)) {
mNavigationBarColor.setBounds(0, 0, size, height);
} else {
mNavigationBarColor.setBounds(0, height - size, width, height);
}
mNavigationBarColor.draw(canvas);
}
mSystemBarBackgroundNode.end(canvas);
mRenderer.drawRenderNode(mSystemBarBackgroundNode);
}
/** Notify view root that a frame has been drawn by us, if it has requested so. */
private void reportDrawIfNeeded() {
if (mReportNextDraw) {
if (mDecorView.isAttachedToWindow()) {
mDecorView.getViewRootImpl().reportDrawFinish();
}
mReportNextDraw = false;
}
}
/**
* 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.
*
* @param drawImmediate if we should draw immediately instead of scheduling a frame
*/
private void pingRenderLocked(boolean drawImmediate) {
if (mChoreographer != null && !drawImmediate) {
mChoreographer.postFrameCallback(this);
} else {
doFrameUncheckedLocked();
}
}
void setUserCaptionBackgroundDrawable(Drawable userCaptionBackgroundDrawable) {
mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable;
}
}