安卓 APP 全局黑白化实现方案
作者:老_匡
https://juejin.cn/post/7167300200921301028
前言
在清明节时各大APP都会进行黑白化处理,当时在接到这个需求的时候感觉好麻烦,是不是又要搞一套皮肤?
然而在一系列搜索之后,找到了两位大神(鸿洋、U2tzJTNE)的实现方案,其实相当的简单!
让我们一起站在巨人的肩膀上来分析一下原理,并思考会不会有更简便的实现?
一、原理
两位大神的置灰方案是相同的,都能看到一段同样的代码:
Paint mPaint = new Paint();
ColorMatrix mColorMatrix = new ColorMatrix();
// 设置饱和度为0
mColorMatrix.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
他们都用了 Android 提供的 ColorMatrix
(颜色矩阵),将其饱和度设置为0,这样使用 Paint 绘制出来的都是没有饱和度的灰白样式!
然而两位在何时使用Paint绘制时选择了不同方案。
1.1 鸿洋: 重写 draw 方法
如果我们把每个 Activity 的根布局饱和度设置为0是不是就可以了?那根布局是谁?
鸿洋告诉我们的布局最后 setContentView
最后都会设置到一个 R.id.content
的 FrameLayout
当中
我们去自定义一个 GrayFrameLayout
,在 draw
的时候使用这个饱和度为0的画笔,被这个 FrameLayout
包裹的布局都会变成黑白。
// 转载自鸿洋
// https://blog.csdn.net/lmj623565791/article/details/105319752
public class GrayFrameLayout extends FrameLayout {
private Paint mPaint = new Paint();
public GrayFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
}
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
super.dispatchDraw(canvas);
canvas.restore();
}
@Override
public void draw(Canvas canvas) {
canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
canvas.restore();
}
}
然后我们用 GrayFrameLayout
去替换这个 R.id.content
的 FrameLayout,就可以做到将页面黑白化了
1.2 U2tzJTNE:监听 DecorView 的监听
U2tzJTNE大佬使用了另一种巧妙的方案。他先创建了一个具有数据变化感知能力的 ObservableArrayList
(当内容发生变化有回调)。
之后使用反射将 WindowManagerGlobal
内的 mViews
容器(ArrayList,该容器会存放所有的 DecorView
),替换为 ObservableArrayList
,这样就可以监听到每个 DecorView
的创建,并且拿到 View 本身。
拿到 DecorView,那就可以为所欲为了!
大佬使用了 setLayerType(View.LAYER_TYPE_HARDWARE, mPaint)
,对布局进行了重绘。至于为什么要用 LAYER_TYPE_HARDWARE
?因为默认的 View.LAYER_TYPE_NONE
会把 Paint 强制设置 为null。
// 转载自U2tzJTNE
// https://juejin.cn/post/6892277675012915207
public static void enable(boolean enable) {
try {
//灰色调Paint
final Paint mPaint = new Paint();
ColorMatrix mColorMatrix = new ColorMatrix();
mColorMatrix.setSaturation(enable ? 0 : 1);
mPaint.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
//反射获取windowManagerGlobal
@SuppressLint("PrivateApi")
Class<?> windowManagerGlobal = Class.forName("android.view.WindowManagerGlobal");
@SuppressLint("DiscouragedPrivateApi")
java.lang.reflect.Method getInstanceMethod = windowManagerGlobal.getDeclaredMethod("getInstance");
getInstanceMethod.setAccessible(true);
Object windowManagerGlobalInstance = getInstanceMethod.invoke(windowManagerGlobal);
//反射获取mViews
Field mViewsField = windowManagerGlobal.getDeclaredField("mViews");
mViewsField.setAccessible(true);
Object mViewsObject = mViewsField.get(windowManagerGlobalInstance);
//创建具有数据感知能力的ObservableArrayList
ObservableArrayList<View> observerArrayList = new ObservableArrayList<>();
observerArrayList.addOnListChangedListener(new ObservableArrayList.OnListChangeListener() {
@Override
public void onChange(ArrayList list, int index, int count) {
}
@Override
public void onAdd(ArrayList list, int start, int count) {
// 拿到DecorView触发重绘
View view = (View) list.get(start);
if (view != null) {
view.setLayerType(View.LAYER_TYPE_HARDWARE, mPaint);
}
}
@Override
public void onRemove(ArrayList list, int start, int count) {
}
});
//将原有的数据添加到新创建的list
observerArrayList.addAll((ArrayList<View>) mViewsObject);
//替换掉原有的mViews
mViewsField.set(windowManagerGlobalInstance, observerArrayList);
} catch (Exception e) {
e.printStackTrace();
}
}
然后只需要在Application里面调用该方法即可。
1.3 方案分析
两位大佬的方案都非常的棒,咱们理性的来对比一下。
鸿洋老师:使用自定义 FrameLayout 的方案需要一个 BaseActivity 统一设置,稍显麻烦,代码侵入性较强。
U2tzJTNE大佬:方案更加简单、动态,一行代码设置甚至可以做到在当前页从彩色变黑白,但是使用了反射,有一点点性能消耗。
二、简易方案(直接复制)
既然研究明白了大佬的方案,那有没有又不需要反射,设置又简单的方法呢?能不能使用原生方式获取 DecorView
的实例呢?
突然灵光一闪,Application
里面不是有 registerActivityLifecycleCallbacks
这个注册监听方法吗?监听里面的 onActivityCreated
不是可以获取到当前的 Activity
吗?那 DecorView
不就拿到了!
public class StudyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
Paint mPaint = new Paint();
ColorMatrix mColorMatrix = new ColorMatrix();
mColorMatrix.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
// 当Activity创建,我们拿到DecorView,使用Paint进行重绘
View decorView = activity.getWindow().getDecorView();
decorView.setLayerType(View.LAYER_TYPE_HARDWARE, mPaint);
}
....
});
}
}
这样看起来是不是更简单了!使用了APP原生的方法实现了黑白化!当然也有缺点,因为在 Activity
级别设置,无法做到在当前页面即时变为黑白。
三、注意事项
这三种方案因为都使用了颜色矩阵,所以坑都是一样的,请注意。
3.1 启动图windowBackground无法变色
在我们可以设置渲染的时候 windowBackground
已经展示完毕了。
解决方案:只能在当前的包里修改,或者不去理会。
3.2 SurfaceView无法变色
因为我们使用了 setLayerType
进行重绘,而 SurfaceView
是有独立的 Window
,脱离布局内的 Window
,运行在其他线程,不影响主线程的绘制,所以当前方案无法使 SurfaceView
变色。
解决方案:
使用 TextureView
。看下这个 SurfaceView
是否可以设置滤镜,正常都是一些三方或者自制的播放器。
3.3 多进程变色
我们可能会在APP内置小程序,小程序基本是运行在单独的进程中,但是如果我们的黑白配置在运行过程中发生变化,其他进程是无法感知的。
解决方案:使用 MMKV 存储黑白配置,并设置多进程共享,在开启小程序之前都判断一下黑白展示。
四、总结
最后咱们再总结一下黑白化方案: 使用了 ColorMatrix 设置饱和度为 0,设置到 Paint 中,让根布局拿着这个 Paint 去进行重绘。
这样 APP 全局黑白化的介绍就结束了,希望大家读完这篇文章,会对 APP 黑白化有一个更深入的了解。如果我的文章能给大家带来一点点的福利,那在下就足够开心了。
-- END --
推荐阅读