高斯模糊数学理论是根据正态分布,对图片进行一遍过滤,本质是一个低通滤波器。在sdk31中,原生提供了两种类型的模糊;
Background blur: 模糊区域是窗口
Blur behind:模糊区域是整个屏幕
API:
WindowManager
# Background blur
android.view.Window#setBackgroundBlurRadius(int blurRadius)
# Blur behind
android.view.WindowManager.LayoutParams#FLAG_BLUR_BEHIND
android.view.WindowManager.LayoutParams#setBlurBehindRadius(int blurBehindRadius)
# Cross-window blur enabled listener
android.view.WindowManager#isCrossWindowBlurEnabled()
android.view.WindowManager#addCrossWindowBlurEnabledListener(@NonNull Consumer listener)
android.view.WindowManager#removeCrossWindowBlurEnabledListener(Consumer listener)
attrs
Window实现类PhoneWindow新增setBackgroundBlurRadius方法,
public final void setBackgroundBlurRadius(int blurRadius) {
super.setBackgroundBlurRadius(blurRadius);
if (CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED) {
if (mBackgroundBlurRadius != Math.max(blurRadius, 0)) {
mBackgroundBlurRadius = Math.max(blurRadius, 0);
mDecor.setBackgroundBlurRadius(mBackgroundBlurRadius);
}
}
}
可知,进一步调用mDecor.setBackgroundBlurRadius,而DecorView作为顶层View,本质也是个View。从该方法可知,会设置高斯模糊监听,如果没有则默认设置一个,重点是updateBackgroundBlurRadius方法。
void setBackgroundBlurRadius(int blurRadius) {
mOriginalBackgroundBlurRadius = blurRadius;
if (blurRadius > 0) {
if (mCrossWindowBlurEnabledListener == null) {
mCrossWindowBlurEnabledListener = enabled -> {
mCrossWindowBlurEnabled = enabled;
updateBackgroundBlurRadius();
};
getContext().getSystemService(WindowManager.class)
.addCrossWindowBlurEnabledListener(mCrossWindowBlurEnabledListener);
getViewTreeObserver().addOnPreDrawListener(mBackgroundBlurOnPreDrawListener);
} else {
updateBackgroundBlurRadius();
}
} else if (mCrossWindowBlurEnabledListener != null) {
updateBackgroundBlurRadius();
removeBackgroundBlurDrawable();
}
}
跟进updateBackgroundBlurRadius,可知设置高斯模糊target为ViewRootImpl,这个不必多说,是view的根viewParent,注意view.getRootView拿到的是根View,一般是DecorView(也有特例,比如部分系统window就不是DecorView),
private void updateBackgroundBlurRadius() {
if (getViewRootImpl() == null) return;
mBackgroundBlurRadius = mCrossWindowBlurEnabled && mWindow.isTranslucent()
? mOriginalBackgroundBlurRadius : 0;
if (mBackgroundBlurDrawable == null && mBackgroundBlurRadius > 0) {
mBackgroundBlurDrawable = getViewRootImpl().createBackgroundBlurDrawable();
updateBackgroundDrawable();
}
if (mBackgroundBlurDrawable != null) {
mBackgroundBlurDrawable.setBlurRadius(mBackgroundBlurRadius);
}
}
sdk31提供了新的类BackgroundBlurDrawable,继承Drawable,保存到mBackgroundBlurDrawable,并且设置模糊度数,跟进updateBackgroundDrawable,这个就是调用view.setBackgroundDrawable将高斯模糊Drawable设置进去即可,
private void updateBackgroundDrawable() {
// Background insets can be null if super constructor calls setBackgroundDrawable.
if (mBackgroundInsets == null) {
mBackgroundInsets = Insets.NONE;
}
if (mBackgroundInsets.equals(mLastBackgroundInsets)
&& mBackgroundBlurDrawable == mLastBackgroundBlurDrawable
&& mLastOriginalBackgroundDrawable == mOriginalBackgroundDrawable) {
return;
}
Drawable destDrawable = mOriginalBackgroundDrawable;
if (mBackgroundBlurDrawable != null) {
destDrawable = new LayerDrawable(new Drawable[] {mBackgroundBlurDrawable,
mOriginalBackgroundDrawable});
}
if (destDrawable != null && !mBackgroundInsets.equals(Insets.NONE)) {
destDrawable = new InsetDrawable(destDrawable,
mBackgroundInsets.left, mBackgroundInsets.top,
mBackgroundInsets.right, mBackgroundInsets.bottom) {
/**
* Return inner padding so we don't apply the padding again in
* {@link DecorView#drawableChanged()}
*/
@Override
public boolean getPadding(Rect padding) {
return getDrawable().getPadding(padding);
}
};
}
// Call super since we are intercepting setBackground on this class.
super.setBackgroundDrawable(destDrawable);
mLastBackgroundInsets = mBackgroundInsets;
mLastBackgroundBlurDrawable = mBackgroundBlurDrawable;
mLastOriginalBackgroundDrawable = mOriginalBackgroundDrawable;
}
抛出一个问题,如果通过Application.getSystemService(WindowManager.class).addView(view,layoutParams)添加的View怎么模糊呢?由于不是在Activity一层,读取源码发现Application.windowManagerd的窗口存在WindowManagerImpl#mParentWindow成员,但是创建时,
registerService(Context.WINDOW_SERVICE, WindowManager.class,
new CachedServiceFetcher() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx);
}});
发现只传入ContextImpl,而该ctx是newApplication时attach的,因此应用进程唯一,回调另一个构造方法,如下,发现传入的window为null,那么问题来了,在Application添加view时没有window,就没办法getWindow设置高斯模糊了呢,怎么办呢?
public WindowManagerImpl(Context context) {
this(context, null /* parentWindow */, null /* clientToken */);
}
【答案】根据源码,通过反射ViewRootImpl创建高斯模糊Drawable即可,代码如下,注意需要在view.onAttachToWindow回调该方法。
@TargetApi(value = 31)
public static void setBackgroundBlurRadius(View view, int radius) {
if (view == null) {
return;
}
if (view instanceof ViewGroup){
ViewGroup viewGroup = (ViewGroup) view;
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(MATCH_PARENT, MATCH_PARENT);
// 这是添加高斯模糊背景,
BlurView blurView = new BlurView(viewGroup.getContext());
blurView.setBlurLayerColor(viewGroup.getContext().getResources().getColor(R.color.blur_color));
viewGroup.setBackground(null);
viewGroup.addView(blurView, 0, lp);
}
ViewParent target = view.getParent();
while (target != null) {
if (target instanceof ViewRootImpl) {
Drawable blurDrawable = getBackgroundBlurRadius((ViewRootImpl) target, radius);
Drawable realDrawable = view.getBackground();
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{realDrawable, blurDrawable});
view.setBackground(layerDrawable);
Log.d(TAG, "setBackgroundBlurRadius: success " + radius);
return;
}
target = target.getParent();
}
}
private static Drawable getBackgroundBlurRadius(ViewRootImpl target, int radius) {
Drawable bd = ReflectUtils.invokeObject(ViewRootImpl.class, target, "createBackgroundBlurDrawable",
Drawable.class, null);
try {
ReflectUtils.invokeObject(Class.forName("com.android.internal.graphics.drawable.BackgroundBlurDrawable"), bd,
"setBlurRadius", void.class, new Class[]{int.class}, radius);
} catch (ClassNotFoundException e) {
Log.e(TAG, "realSetBackgroundBlurRadius:" + e.getMessage());
}
return bd;
}
上述问题是跨窗口控件模糊,流程简单总结如下:
BackgroundBlurDrawable backgroundBlurDrawable = myView.getViewRootImpl().createBackgroundBlurDrawable();
backgroundBlurDrawable.setBlurRadius(150);
backgroundBlurDrawable.setCornerRadius(10, 10, 10, 10);
myView.setBackground(backgroundBlurDrawable);
添加了新的RenderEffect API,可以作用于RenderNode
renderNode.setRenderEffect(
RenderEffect.createBlurEffect(radiusX, radiusY, Shader.TileMode.CLAMP))
myView.setRenderEffect(
RenderEffect.createBlurEffect(radiusX, radiusY, Shader.TileMode.CLAMP))
API:
android.graphics.RenderNode#setRenderEffect(RenderEffect effect)
android.graphics.RenderEffect#createBlurEffect(float radiusX, float radiusY,
@NonNull TileMode edgeTreatment);