又见清明之 app黑白化
背景
疫情已经持续了两年之久,从去年清明节各个APP陆续在节日当天将APP黑白化,以示对逝者的尊敬和祭奠。除了悼念,作为一个开发者,更需要理解这背后的技术即黑白化的技术。
一、带零点技术的方案
其实出于对逝者的尊重,只要APP首页不出现艳色或者明亮色,即将所有艳色的文字和 图片 替换成 暗色系抑或黑白色的即可,但这种方案基本没有可复用性 并且开发成本很大,这种方案其实没用到什么技术,那作为技术人,是否可以考虑下一丁点技术呢?
二、带一点技术的方案
考虑到改变图片 或者 文字颜色,很容易就想到了绘制,也就是和 onDraw 有关,那我们能不能通过自定义view来实现呢 ? 答案当然是可以的 ,我们通过自定义view ,通过复写 onDraw即可将背景改为黑白
public GrayView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
}
public GrayView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.saveLayer(null,mPaint,Canvas.ALL_SAVE_FLAG);
super.onDraw(canvas);
canvas.restore();
}
我们通过自定义view 通过 ColorMatrix.setSaturation(0) 构建黑白的矩阵,然后通过 mPaint 来绘制达到黑白化效果。为什么ColorMatrix.setSaturation(0) 可以达到黑白化的效果呢?我们看下注释:
/**
* Set the matrix to affect the saturation of colors.
*
* @param sat A value of 0 maps the color to gray-scale. 1 is identity.
*/
public void setSaturation(float sat) {
reset();
float[] m = mArray;
final float invSat = 1 - sat;
final float R = 0.213f * invSat;
final float G = 0.715f * invSat;
final float B = 0.072f * invSat;
m[0] = R + sat; m[1] = G; m[2] = B;
m[5] = R; m[6] = G + sat; m[7] = B;
m[10] = R; m[11] = G; m[12] = B + sat;
}
注释传入0 即可达到黑白效果,通过修改颜色矩阵,paint绘图达到黑白灰效果。
但是这种方案需要定义很多自定义View,相比第一种没有多少进步性可言,但是至少我们开始用技术了,那么我们有带两点 或者三点 或者更多的技术么?
三、带两点技术的方案
在方案二的基础上,既然所有控件都是view ,自定义所有控件的开发量巨大,那么对于viewGroup 来说如果自定义了viewgroup实现黑白化,paint、canvas又可以通过父VIEW传递给子View,那也就意味着我们只需要替换页面的根布局,只用自定义这个ViewGroup 就行。
关键代码:
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.saveLayer(null,mPaint,Canvas.ALL_SAVE_FLAG);
super.dispatchDraw(canvas);
canvas.restore();
}
我们可以在我们app 中baseActivity 中 替换掉我们自定义的viewGroup 来替换 根布局:
@Nullable
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
if("FrameLayout".equals(name)){
int count = attrs.getAttributeCount();
for(int i = 0; i < count;i++){
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
if(attrName.equals("id")){
int id = Integer.parseInt(attrValue.substring(1));
String idValue = getResources().getResourceName(id);
if("android:id/content".equals(idValue)){
GrayFramLayout grayFramLayout = new GrayFramLayout(context,attrs);
return grayFramLayout;
}
}
}
}
return super.onCreateView(name, context, attrs);
}
这个方案我们细想下,有什么缺点么? baseActivity 是APP中的基类,那第三方页面呢,没有继承我们的baseActivity,是不是就可以一直逍遥法外了? 接下来我们再看一种方案。
四、带三点技术的方案
既然我们可以替换android:id/content,那我们是不是也可以替换DecorView
参考代码如下:
参考代码如下:
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Paint mPaint = new Paint();
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
getWindow().getDecorView().setLayerType(View.LAYER_TYPE_HARDWARE,mPaint);
initView();
}
上述代码经过测试,对于基类是AppCompatActivity 的页面不生效,似乎对上一种方案又有了倒退,谷歌中途对于AppCompatActivity 采用setFactory 做了一层转换,那既然提到了setFactory,那我们是不是也可以在这个上边做做文章?
五、终极解决方案
既然方案3 和 方案4 各有千秋,那各取所长是不是就可以达到一种完美的解决方案,答案是确定的 。另外我们再加上 lifeCycleCallback ,就可以达到所有的 Activity 都生效。
主要代码:
Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.getWindow() != null
&& activity.getWindow().getDecorView() != null) {
setViewGray(activity.getWindow().getDecorView());
}
LayoutInflater layoutInflater = activity.getLayoutInflater();
if (layoutInflater == null) {
return;
}
// 获取 是否设置过factory
boolean isFactorySet = getFactorySet(layoutInflater);
if (isFactorySet) {
// 设置mFactorySet
setFactorySet(layoutInflater);
// 重新读mFactorySet
isFactorySet = getFactorySet(layoutInflater);
}
if (!isFactorySet) {
try {
LayoutInflaterCompat.setFactory2(activity.getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if ("FrameLayout".equals(name)) {
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
if (attrName.equals("id")) {
int id = Integer.parseInt(attrValue.substring(1));
String idValue = context.getResources().getResourceName(id);
if ("android:id/content".equals(idValue)) {
FrameLayout grayFrameLayout = new FrameLayout(context, attrs);
setViewGray(grayFrameLayout);
return grayFrameLayout;
}
}
}
}
return null;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
} catch (Exception e) {
WLog.e(TAG, e.getMessage());
}
}
}
第一层通过设置decordView 达到黑白化,第二层通过setFactory2 来替换根布局来达到黑白化。
六、关于黑白化不可逆的原理
上述方案可以将页面黑白化,但黑白化的页面,想通过颜色矩阵显示正常,这个臣妾办不到呀!为什么呢?
我们先看下原理:
灰度图片的去色原理:只要把 RGB 的三色通道的数值设置为一样,即 R=G=B,那么图像就变成了灰色,同时为了 保证图像的亮度,需要使同一个通道中的 R+G+B 的结果接近1。
- 在 matlab 中按照 0.2989 R,0.5870 G 和 0.1140 B 的比例构成像素灰度值
- 在 OpenCV 中按照 0.299 R, 0.587 G 和 0.114 B 的比例构成像素灰度值
- 在 Android 中按照0.213 R,0.715 G 和 0.072 B 的比例构成像素灰度值
按照如上原理,要求运算后:
R'=G‘=B' = 0.213R+0.715G+0.072B 即可实现黑白化
于是得出如下矩阵:
[ 0.213f 0.715f 0.072f 0 0
0.213f 0.715f 0.072f 0 0
0.213f 0.715f 0.072f 0 0
0 0 0 1 0 ]
与颜色分量矩阵相乘后即可黑白化成功,源码setSaturation(0f)时得到的矩阵和上述一致。
我们先假设是可逆的,即可以恢复部分区域正常:
黑白化后 :
R'=G‘=B' = 0.213R+0.715G+0.072B ,及新的颜色分量矩阵 C':
[ R'
G'
B'
A'
1]
假设是可逆的,我们需要先假设一个矩阵M'如下:
[ x, y, z, 0, 0,
a, b, c, 0, 0,
d, e, f, 0, 0,
0, 0, 0, 1, 0 ]
需要和新的颜色分量矩阵运算后 及 M' * C' 需要得到结果:R、G、B、A :
可以得到方程式:
方程式一:(x+y+z)*0.213R + (x+y+z)*0.715G + (x+y+z)*0.072B = R
方程式二:(a+b+c)*0.213R + (a+b+c)*0.715G + (a+b+c)*0.072B = G
方程式三:(d+e+f)*0.213R + (d+e+f)*0.715G + (d+e+f)*0.072B = B
我们以方程式一来举例:
以R来举例:需要x+y+z = 1/0.213;
以R的G分量来举例:需要x+y+z = 0;
综上两个可以知道 x,y,z 是无解的(a,b,c / d,e,f)同理。
由于x/y/z 处于无解状态,所以局部恢复难以实现,这也是上述方案的缺点
转载自:https://juejin.cn/post/6947861691426144292