blob: 402d7fed90c5a7bef5a144b03d3f7334d485bd93 [file] [log] [blame]
/*
* Copyright (C) 2020 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.graphics.drawable;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RenderNode;
import android.graphics.drawable.Drawable;
import android.util.ArraySet;
import android.util.Log;
import android.util.LongSparseArray;
import android.view.ViewRootImpl;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
/**
* A drawable that keeps track of a blur region, pokes a hole under it, and propagates its state
* to SurfaceFlinger.
*/
public final class BackgroundBlurDrawable extends Drawable {
private static final String TAG = BackgroundBlurDrawable.class.getSimpleName();
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final Aggregator mAggregator;
private final RenderNode mRenderNode;
private final Paint mPaint = new Paint();
private final Path mRectPath = new Path();
private final float[] mTmpRadii = new float[8];
private boolean mVisible = true;
// Confined to UiThread. The values are copied into a BlurRegion, which lives on
// RenderThread to avoid interference with UiThread updates.
private int mBlurRadius;
private float mCornerRadiusTL;
private float mCornerRadiusTR;
private float mCornerRadiusBL;
private float mCornerRadiusBR;
private float mAlpha = 1;
// Do not update from UiThread. This holds the latest position for this drawable. It is used
// by the Aggregator from RenderThread to get the final position of the blur region sent to SF
private final Rect mRect = new Rect();
// This is called from a thread pool. The callbacks might come out of order w.r.t. the frame
// number, so we send a Runnable holding the actual update to the Aggregator. The Aggregator
// can apply the update on RenderThread when processing that same frame.
@VisibleForTesting
public final RenderNode.PositionUpdateListener mPositionUpdateListener =
new RenderNode.PositionUpdateListener() {
@Override
public void positionChanged(long frameNumber, int left, int top, int right,
int bottom) {
mAggregator.onRenderNodePositionChanged(frameNumber, () -> {
mRect.set(left, top, right, bottom);
});
}
@Override
public void positionLost(long frameNumber) {
mAggregator.onRenderNodePositionChanged(frameNumber, () -> {
mRect.setEmpty();
});
}
};
private BackgroundBlurDrawable(Aggregator aggregator) {
mAggregator = aggregator;
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
mPaint.setColor(Color.TRANSPARENT);
mPaint.setAntiAlias(true);
mRenderNode = new RenderNode("BackgroundBlurDrawable");
mRenderNode.addPositionUpdateListener(mPositionUpdateListener);
}
@Override
public void draw(@NonNull Canvas canvas) {
if (mRectPath.isEmpty() || !isVisible() || getAlpha() == 0) {
return;
}
canvas.drawPath(mRectPath, mPaint);
canvas.drawRenderNode(mRenderNode);
}
/**
* Color that will be alpha blended on top of the blur.
*/
public void setColor(@ColorInt int color) {
mPaint.setColor(color);
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
boolean changed = super.setVisible(visible, restart);
if (changed) {
mVisible = visible;
mAggregator.onBlurDrawableUpdated(this);
}
return changed;
}
@Override
public void setAlpha(int alpha) {
if (mAlpha != alpha / 255f) {
mAlpha = alpha / 255f;
invalidateSelf();
mAggregator.onBlurDrawableUpdated(this);
}
}
/**
* Blur radius in pixels.
*/
public void setBlurRadius(int blurRadius) {
if (mBlurRadius != blurRadius) {
mBlurRadius = blurRadius;
invalidateSelf();
mAggregator.onBlurDrawableUpdated(this);
}
}
/**
* Sets the corner radius, in degrees.
*/
public void setCornerRadius(float cornerRadius) {
setCornerRadius(cornerRadius, cornerRadius, cornerRadius, cornerRadius);
}
/**
* Sets the corner radius in degrees.
* @param cornerRadiusTL top left radius.
* @param cornerRadiusTR top right radius.
* @param cornerRadiusBL bottom left radius.
* @param cornerRadiusBR bottom right radius.
*/
public void setCornerRadius(float cornerRadiusTL, float cornerRadiusTR, float cornerRadiusBL,
float cornerRadiusBR) {
if (mCornerRadiusTL != cornerRadiusTL
|| mCornerRadiusTR != cornerRadiusTR
|| mCornerRadiusBL != cornerRadiusBL
|| mCornerRadiusBR != cornerRadiusBR) {
mCornerRadiusTL = cornerRadiusTL;
mCornerRadiusTR = cornerRadiusTR;
mCornerRadiusBL = cornerRadiusBL;
mCornerRadiusBR = cornerRadiusBR;
updatePath();
invalidateSelf();
mAggregator.onBlurDrawableUpdated(this);
}
}
@Override
public void setBounds(int left, int top, int right, int bottom) {
super.setBounds(left, top, right, bottom);
mRenderNode.setPosition(left, top, right, bottom);
updatePath();
}
private void updatePath() {
mTmpRadii[0] = mTmpRadii[1] = mCornerRadiusTL;
mTmpRadii[2] = mTmpRadii[3] = mCornerRadiusTR;
mTmpRadii[4] = mTmpRadii[5] = mCornerRadiusBL;
mTmpRadii[6] = mTmpRadii[7] = mCornerRadiusBR;
mRectPath.reset();
if (getAlpha() == 0 || !isVisible()) {
return;
}
Rect bounds = getBounds();
mRectPath.addRoundRect(bounds.left, bounds.top, bounds.right, bounds.bottom, mTmpRadii,
Path.Direction.CW);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
throw new IllegalArgumentException("not implemented");
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public String toString() {
return "BackgroundBlurDrawable{"
+ "blurRadius=" + mBlurRadius
+ ", corners={" + mCornerRadiusTL
+ "," + mCornerRadiusTR
+ "," + mCornerRadiusBL
+ "," + mCornerRadiusBR
+ "}, alpha=" + mAlpha
+ ", visible=" + mVisible
+ "}";
}
/**
* Responsible for keeping track of all blur regions of a {@link ViewRootImpl} and posting a
* message when it's time to propagate them.
*/
public static final class Aggregator {
private final Object mRtLock = new Object();
// Maintains a list of all *visible* blur drawables. Confined to UI thread
private final ArraySet<BackgroundBlurDrawable> mDrawables = new ArraySet();
@GuardedBy("mRtLock")
private final LongSparseArray<ArraySet<Runnable>> mFrameRtUpdates = new LongSparseArray();
private final ViewRootImpl mViewRoot;
private BlurRegion[] mTmpBlurRegionsForFrame = new BlurRegion[0];
private boolean mHasUiUpdates;
public Aggregator(ViewRootImpl viewRoot) {
mViewRoot = viewRoot;
}
/**
* Creates a blur region with default radius.
*/
public BackgroundBlurDrawable createBackgroundBlurDrawable(Context context) {
BackgroundBlurDrawable drawable = new BackgroundBlurDrawable(this);
drawable.setBlurRadius(context.getResources().getDimensionPixelSize(
R.dimen.default_background_blur_radius));
return drawable;
}
/**
* Called when a BackgroundBlurDrawable has been updated
*/
@UiThread
void onBlurDrawableUpdated(BackgroundBlurDrawable drawable) {
final boolean shouldBeDrawn =
drawable.mAlpha != 0 && drawable.mBlurRadius > 0 && drawable.mVisible;
final boolean isDrawn = mDrawables.contains(drawable);
if (shouldBeDrawn) {
mHasUiUpdates = true;
if (!isDrawn) {
mDrawables.add(drawable);
if (DEBUG) {
Log.d(TAG, "Add " + drawable);
}
} else {
if (DEBUG) {
Log.d(TAG, "Update " + drawable);
}
}
} else if (!shouldBeDrawn && isDrawn) {
mHasUiUpdates = true;
mDrawables.remove(drawable);
if (DEBUG) {
Log.d(TAG, "Remove " + drawable);
}
}
}
// Called from a thread pool
void onRenderNodePositionChanged(long frameNumber, Runnable update) {
// One of the blur region's position has changed, so we have to send an updated list
// of blur regions to SurfaceFlinger for this frame.
synchronized (mRtLock) {
ArraySet<Runnable> frameRtUpdates = mFrameRtUpdates.get(frameNumber);
if (frameRtUpdates == null) {
frameRtUpdates = new ArraySet<>();
mFrameRtUpdates.put(frameNumber, frameRtUpdates);
}
frameRtUpdates.add(update);
}
}
/**
* @return true if there are any updates that need to be sent to SF
*/
@UiThread
public boolean hasUpdates() {
return mHasUiUpdates;
}
/**
* @return true if there are any visible blur regions
*/
@UiThread
public boolean hasRegions() {
return mDrawables.size() > 0;
}
/**
* @return an array of BlurRegions, which are holding a copy of the information in
* all the currently visible BackgroundBlurDrawables
*/
@UiThread
public BlurRegion[] getBlurRegionsCopyForRT() {
if (mHasUiUpdates) {
mTmpBlurRegionsForFrame = new BlurRegion[mDrawables.size()];
for (int i = 0; i < mDrawables.size(); i++) {
mTmpBlurRegionsForFrame[i] = new BlurRegion(mDrawables.valueAt(i));
}
mHasUiUpdates = false;
}
return mTmpBlurRegionsForFrame;
}
/**
* Called on RenderThread.
*
* @return all blur regions if there are any ui or position updates for this frame,
* null otherwise
*/
@VisibleForTesting
public float[][] getBlurRegionsToDispatchToSf(long frameNumber,
BlurRegion[] blurRegionsForFrame, boolean hasUiUpdatesForFrame) {
synchronized (mRtLock) {
if (!hasUiUpdatesForFrame && (mFrameRtUpdates.size() == 0
|| mFrameRtUpdates.keyAt(0) > frameNumber)) {
return null;
}
// mFrameRtUpdates holds position updates coming from a thread pool span from
// RenderThread. At this point, all position updates for frame frameNumber should
// have been added to mFrameRtUpdates.
// Here, we apply all updates for frames <= frameNumber in case some previous update
// has been missed. This also protects mFrameRtUpdates from memory leaks.
while (mFrameRtUpdates.size() != 0 && mFrameRtUpdates.keyAt(0) <= frameNumber) {
final ArraySet<Runnable> frameUpdates = mFrameRtUpdates.valueAt(0);
mFrameRtUpdates.removeAt(0);
for (int i = 0; i < frameUpdates.size(); i++) {
frameUpdates.valueAt(i).run();
}
}
}
if (DEBUG) {
Log.d(TAG, "Dispatching " + blurRegionsForFrame.length + " blur regions:");
}
final float[][] blurRegionsArray = new float[blurRegionsForFrame.length][];
for (int i = 0; i < blurRegionsArray.length; i++) {
blurRegionsArray[i] = blurRegionsForFrame[i].toFloatArray();
if (DEBUG) {
Log.d(TAG, blurRegionsForFrame[i].toString());
}
}
return blurRegionsArray;
}
/**
* Called on RenderThread in FrameDrawingCallback.
* Dispatch all blur regions if there are any ui or position updates.
*/
public void dispatchBlurTransactionIfNeeded(long frameNumber,
BlurRegion[] blurRegionsForFrame, boolean hasUiUpdatesForFrame) {
final float[][] blurRegionsArray = getBlurRegionsToDispatchToSf(frameNumber,
blurRegionsForFrame, hasUiUpdatesForFrame);
if (blurRegionsArray != null) {
mViewRoot.dispatchBlurRegions(blurRegionsArray, frameNumber);
}
}
}
/**
* Wrapper for sending blur data to SurfaceFlinger
* Confined to RenderThread.
*/
public static final class BlurRegion {
public final int blurRadius;
public final float cornerRadiusTL;
public final float cornerRadiusTR;
public final float cornerRadiusBL;
public final float cornerRadiusBR;
public final float alpha;
public final Rect rect;
BlurRegion(BackgroundBlurDrawable drawable) {
alpha = drawable.mAlpha;
blurRadius = drawable.mBlurRadius;
cornerRadiusTL = drawable.mCornerRadiusTL;
cornerRadiusTR = drawable.mCornerRadiusTR;
cornerRadiusBL = drawable.mCornerRadiusBL;
cornerRadiusBR = drawable.mCornerRadiusBR;
rect = drawable.mRect;
}
/**
* Serializes this class into a float array that's more JNI friendly.
*/
float[] toFloatArray() {
final float[] floatArray = new float[10];
floatArray[0] = blurRadius;
floatArray[1] = alpha;
floatArray[2] = rect.left;
floatArray[3] = rect.top;
floatArray[4] = rect.right;
floatArray[5] = rect.bottom;
floatArray[6] = cornerRadiusTL;
floatArray[7] = cornerRadiusTR;
floatArray[8] = cornerRadiusBL;
floatArray[9] = cornerRadiusBR;
return floatArray;
}
@Override
public String toString() {
return "BlurRegion{"
+ "blurRadius=" + blurRadius
+ ", corners={" + cornerRadiusTL
+ "," + cornerRadiusTR
+ "," + cornerRadiusBL
+ "," + cornerRadiusBR
+ "}, alpha=" + alpha
+ ", rect=" + rect
+ "}";
}
}
}