Android APP global black and white realization scheme

Thank you for joining me in fighting monsters and upgrading in the Android world!

During the Ching Ming Festival, all major apps will be processed in black and white. When I received this request, I felt very troublesome. Do I need to create another skin?

However, after a series of searches, I found the implementation plan of two great gods ( Hong Yang , U2tzJTNE ), which is actually quite simple!

Let us stand on the shoulders of giants to analyze the principle, and think about whether there is an easier implementation?

1. Principle

The graying schemes of the two masters are the same, and they can both see the same piece of code:

Paint mPaint = new Paint();
ColorMatrix mColorMatrix = new ColorMatrix();
// 设置饱和度为0
mColorMatrix.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
复制代码

They all use the ColorMatrix (color matrix) provided by Android, and set its saturation to 0, so that the gray and white styles without saturation are all drawn using Paint!

However, the two chose different schemes when using Paint to draw.

1.1 Hongyang: Override the draw method

Teacher Hongyang analyzed, if we set the root layout saturation of each Activity to 0 , is it all right?

Who is that layout?

Mr. Hongyang analyzed our layout and finally setContentView will be set to a FrameLayout of R.id.content.

Let's customize a GrayFrameLayout, use this brush with a saturation of 0 when drawing, and the layout wrapped by this FrameLayout will become black and white.

// 转载自鸿洋
// 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();
    }
}
复制代码

Then we use GrayFrameLayout to replace the FrameLayout of this R.id.content, can we make the page black and white?

The method of replacing FrameLayout can be viewed under the article of [Hong Yang] .

1.2 U2tzJTNE: Monitor the addition of DecorView

The U2tzJTNE boss used another ingenious scheme.

他先创建了一个具有数据变化感知能力的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);
            }
           
			....
        });
    }
}
复制代码

Doesn't it look simpler this way! Using the APP native method to achieve black and white! Of course, there are also disadvantages, because the settings at the Activity level cannot instantly change the current page to black and white.

3. Matters needing attention

These three schemes all use the color matrix, so the pits are the same, please note.

3.1 The windowBackground of the startup image cannot change color

The windowBackground is already displayed by the time we can set up rendering.

Solution: You can only modify it in the current package, or ignore it.

3.2 SurfaceView cannot change color

Because we use setLayerType for redrawing, and SurfaceView has an independent Window, which is separated from the Window in the layout and runs on other threads without affecting the drawing of the main thread, so the current solution cannot change the color of SurfaceView.

Solution: 1. Use TextureView. 2. Check whether this SurfaceView can set a filter, usually some third-party or self-made players.

3.3 Multi-process color change

We may have a built-in applet in the APP, and the applet basically runs in a separate process, but if our black and white configuration changes during the running process, other processes cannot perceive it.

Solution: Use MMKV to store the black and white configuration, and set up multi-process sharing, and judge the black and white display before opening the applet.

Summarize

Finally, let's summarize the black and white scheme.

Use ColorMatrix to set the saturation to 0, set it to Paint, and let the root layout take this Paint to redraw.

This concludes the introduction to the global black and white of APP. I hope that after reading this article, you will have a deeper understanding of the black and white of APP. If my article can bring you a little benefit, then I will be happy enough.

see you later!

Guess you like

Origin juejin.im/post/7167300200921301028