App全局灰度化实践-GlobalGray

4,427 阅读4分钟

这是一篇迟到了许久的文章,因工作原因一直没时间写(好吧,其实就是懒)。。

导语

清明节当天,各大公司都将自己的应用设成了全局灰色调,以哀悼逝去的同胞,作为程序猿的我们,当然要研究一下此技术的实习方案。


思路

首先,核心代码如下:

Paint paint = new Paint();
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
view.setLayerType(View.LAYER_TYPE_HARDWARE, paint);

上述代码原理是:

创建一个饱和度为0的颜色过滤器(饱和度为0时,view就变成了黑白色),将这个颜色过滤器通过setLayerType方法设置到当前View上。(此方法会开启此View的硬件加速功能)

确定了原理,我们现在要确定代码执行的时机:

因为我们要实现的功能是全局灰度化,所以首先view的选型最好是一个根view,这样我们就不用在每个子view上重复操作了,其次我们还要适配Dialog和PopWindow等Window类型的组件,所以我们不能从Activity上动心思了,因为Activity本质也是一个Window,所以综上所述,我们将代码的执行时机选在了WindowManager的addView方法上面,因为不论是Activity的创建还是Dialog等组件的创建最终都会走到这个方法里面。

我们只要在WindowManager执行addView方法时进行hook,加入我们上面的代码逻辑即可。

实现

上面确定了大体思路,我们现在来进一步分析实现方案:

首先,既然是要进行hook操作,那么我们就要先确定hook点:

WindowManager是一个接口类,其实现类为WindowManagerImpl,而WindowManagerImpl中真正实现窗口操作的类为WindowManagerGlobal,是一个单例类,我们看一下其addView的实现:

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
    ....
	mViews.add(view);
    ....
}

可以发现方法内部将view保存到了一个list里面,那我们可以hook这个list,在其添加view的时候,我们就调用上面的代码将其置灰,那如何才能感知到view的添加操作呢?

实现具有数据感知能力的list:

我们可以自定义一个数组,让其继承于 ArrayList,在其执行添加删除操作时,通过回调进行通知,具体实现如下:

public class ObservableArrayList<T> extends ArrayList<T> {
    private List<OnListChangeListener> mListeners = new ArrayList<>();

    public void addOnListChangedListener(OnListChangeListener listener) {
        if (mListeners != null) {
            mListeners.add(listener);
        }
    }

    public void removeOnListChangedListener(OnListChangeListener listener) {
        if (mListeners != null) {
            mListeners.remove(listener);
        }
    }

    @Override
    public boolean add(T object) {
        super.add(object);
        notifyAdd(size() - 1, 1);
        return true;
    }

    @Override
    public void add(int index, T object) {
        super.add(index, object);
        notifyAdd(index, 1);
    }

    @Override
    public boolean addAll(Collection<? extends T> collection) {
        int oldSize = size();
        boolean added = super.addAll(collection);
        if (added) {
            notifyAdd(oldSize, size() - oldSize);
        }
        return added;
    }


    @Override
    public void clear() {
        int oldSize = size();
        super.clear();
        if (oldSize != 0) {
            notifyRemove(0, oldSize);
        }
    }

    @Override
    public T remove(int index) {
        T val = super.remove(index);
        notifyRemove(index, 1);
        return val;
    }

    @Override
    public boolean remove(Object object) {
        int index = indexOf(object);
        if (index >= 0) {
            remove(index);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public T set(int index, T object) {
        T val = super.set(index, object);
        if (mListeners != null) {
            for (OnListChangeListener listener : mListeners) {
                listener.onChange(this, index, 1);
            }
        }
        return val;
    }

    @Override
    protected void removeRange(int fromIndex, int toIndex) {
        super.removeRange(fromIndex, toIndex);
        notifyRemove(fromIndex, toIndex - fromIndex);
    }

    private void notifyAdd(int start, int count) {
        if (mListeners != null) {
            for (OnListChangeListener listener : mListeners) {
                listener.onAdd(this, start, count);
            }
        }
    }

    private void notifyRemove(int start, int count) {
        if (mListeners != null) {
            for (OnListChangeListener listener : mListeners) {
                listener.onRemove(this, start, count);
            }
        }
    }

    public interface OnListChangeListener {

        void onChange(ArrayList list, int index, int count);

        void onAdd(ArrayList list, int start, int count);

        void onRemove(ArrayList list, int start, int count);
    }
}

接下来就是执行hook操作:

/**
 * @param enable 是否开启全局灰色调
 */
public static void enable(boolean enable) {
    if (!enable) {
        return;
    }
    try {
        //灰色调Paint
        final Paint mPaint = new Paint();
        ColorMatrix mColorMatrix = new ColorMatrix();
        mColorMatrix.setSaturation(0);
        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) {
                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的onCreate中调用上述代码进行hook即可。

已知存在的问题

  1. 开启了View的硬件加速,可能会存在兼容性问题
  2. 对于SurfaceView和Textureview以及其子类无效

对于问题1笔者在项目中暂未遇到问题,如果读者朋友们在项目中遇到兼容性问题,欢迎留言;

对于问题2其原因是因为这两个组件内部有独立的绘图表面(Surface),笔者目前未想到好的解决方案,如果有思路欢迎一块交流。

组件封装

为了方便大家使用,我将上述代码封装成了组件并上传到了jitpack,使用方法如下:

首先在你项目的build.gradle文件中添加jitpack的仓库

allprojects {
   repositories {
      ...
        maven { url 'https://jitpack.io' }
    }
}

添加依赖:

implementation 'com.github.U2tzJTNE:GlobalGray:1.0'

Application的onCreate中调用:

GlobalGray.enable(true);

效果演示

demo

代码地址

github.com/U2tzJTNE/Gl…