• 安卓APP全局黑白化实现方案


    感谢大家和我一起,在Android世界打怪升级!

    在清明节时各大APP都会进行黑白化处理,当时在接到这个需求的时候感觉好麻烦,是不是又要搞一套皮肤?

    然而在一系列搜索之后,找到了两位大神(鸿洋U2tzJTNE)的实现方案,其实相当的简单!

    让我们一起站在巨人的肩膀上来分析一下原理,并思考会不会有更简便的实现?

    一、原理

    两位大神的置灰方案是相同的,都能看到一段同样的代码:

    Paint mPaint = new Paint();
    ColorMatrix mColorMatrix = new ColorMatrix();
    // 设置饱和度为0
    mColorMatrix.setSaturation(0);
    mPaint.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
    
    • 1
    • 2
    • 3
    • 4
    • 5

    他们都用了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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    然后我们用GrayFrameLayout去替换这个R.id.content的FrameLayout,是不是就可以做到将页面黑白化了?

    替换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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    只需要在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);
                }
               
    			....
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这样看起来是不是更简单了!使用了APP原生的方法实现了黑白化!当然也有缺点,因为在Activity级别设置,无法做到在当前页面即时变为黑白。

    三、注意事项

    这三种方案因为都使用了颜色矩阵,所以坑都是一样的,请注意。

    3.1 启动图windowBackground无法变色

    在我们可以设置渲染的时候windowBackground已经展示完毕了。

    解决方案:只能在当前的包里修改,或者不去理会。

    3.2 SurfaceView无法变色

    因为我们使用了setLayerType进行重绘,而SurfaceView是有独立的Window,脱离布局内的Window,运行在其他线程,不影响主线程的绘制,所以当前方案无法使SurfaceView变色。

    解决方案:
    1、使用TextureView。
    2、看下这个SurfaceView是否可以设置滤镜,正常都是一些三方或者自制的播放器。

    3.3 多进程变色

    我们可能会在APP内置小程序,小程序基本是运行在单独的进程中,但是如果我们的黑白配置在运行过程中发生变化,其他进程是无法感知的。

    解决方案:使用MMKV存储黑白配置,并设置多进程共享,在开启小程序之前都判断一下黑白展示。

    总结

    最后咱们再总结一下黑白化方案。

    使用了ColorMatrix设置饱和度为0,设置到Paint中,让根布局拿着这个Paint去进行重绘。

    这样APP全局黑白化的介绍就结束了,希望大家读完这篇文章,会对APP黑白化有一个更深入的了解。如果我的文章能给大家带来一点点的福利,那在下就足够开心了。

    下次再见!

  • 相关阅读:
    Linux 基础IO
    【Hello Algorithm】二叉树的递归套路
    java毕业生设计校园社团管理系统计算机源码+系统+mysql+调试部署+lw
    中国科学院大学计算机考研资料汇总
    YOLO V2详解
    通讯协议学习之路:USART协议理论
    面对网络渠道低价 品牌如何应对
    Golang中的GC回收机制:三色标记与混合写屏障
    【小程序项目开发--京东商城】uni-app之自定义搜索组件(上)-- 组件UI
    Linux C语言:多级指针(void指针和const)
  • 原文地址:https://blog.csdn.net/kuanggang_android/article/details/127922333