App 黑白化技术实践上篇

本文正在参与「金石计划 . 分割6万现金大奖」

前言

很快乐遇见你~

最近翻开各大 App 会发现它们都做了是非化,如下支付宝的处理:

App 黑白化技术实践上篇

能够看到运用设置了全局灰色调,表达了一种对逝者的哀悼,非常的应景和人性化。作为程序猿,咱们来探索一下它从技能视点是怎样完成的。

Github Demo 地址:github.com/sweetying52…

一、App 是非化完成原理

1.1、修正 Canvas 的 Paint 完成是非化

首要咱们应该知道 Android 中能完成是非化的手法:

正常情况下,App 页面上的 View 都是经过 Canvas + Paint 画出来的。Canvas 对应画布,Paint 对应画笔,两者结合,就能画出 View。

就比如画家画画,如下图:

App 黑白化技术实践上篇

画一幅画他需求有画布和画笔,经过不同颜色的画笔结合,就画出了一幅活灵活现的画。

到这儿你是否受到了一点启示:在 Canvas 上制作 View 的时分,咱们换一支颜色饱和度为 0 的 Paint(画笔),是否就能画出是非化的 View 呢?

感觉可行,找一下 Paint 相关的 Api ,发现能够对 Paint 进行如下设置:

//新建一支画笔
val paint = Paint()
//经过 ColorMatrix 将饱和度设置为 0
val cm = ColorMatrix()
cm.setSaturation(0f)
//将画笔的颜色饱和度设置为 0
paint.colorFilter = ColorMatrixColorFilter(cm)

上述代码咱们就新建了一支颜色饱和度为 0 的 Paint,接下来运用它去进行 View 的制作,就能到达是非化的作用。

咱们进行一个简略的测验:

1、自定义是非化 TextView 和 Button,代码如下:

//1、自定义是非化 TextView
class GrayTextView(context: Context, attrs: AttributeSet): TextView(context,attrs) {
    //颜色饱和度为 0 的 Paint
    private val paint by lazy {
        val p = Paint()
        val cm = ColorMatrix()
        cm.setSaturation(0f)
        p.colorFilter = ColorMatrixColorFilter(cm)
        p
    }
    override fun draw(canvas: Canvas?) {
        canvas?.saveLayer(null,paint, Canvas.ALL_SAVE_FLAG)
        super.draw(canvas)
        canvas?.restore()
    }
}
//2、自定义是非化 button
class GrayButton(context: Context, attrs: AttributeSet): Button(context,attrs) {
    //颜色饱和度为 0 的 Paint
    private val paint by lazy {
        val p = Paint()
        val cm = ColorMatrix()
        cm.setSaturation(0f)
        p.colorFilter = ColorMatrixColorFilter(cm)
        p
    }
    override fun draw(canvas: Canvas?) {
        canvas?.saveLayer(null,paint, Canvas.ALL_SAVE_FLAG)
        super.draw(canvas)
        canvas?.restore()
    }
}

2、修正 activity.main.xml 的布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:padding="20dp"
    tools:context=".MainActivity">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="erdai666"
        android:textColor="@android:color/holo_green_light"
        android:textSize="30sp"/>
    <com.dream.appblackandwhite.blackandwhitewidget.GrayTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="erdai666"
        android:textColor="@android:color/holo_green_light"
        android:textSize="30sp"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/holo_green_light"
        android:text="erdai666" />
    <com.dream.appblackandwhite.blackandwhitewidget.GrayButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="erdai666"
        android:textColor="@android:color/holo_green_light" />
</LinearLayout>

布局很简略,便是和未是非化的 TextView 和 Button 做对比

3、运转 app ,作用如下图:

App 黑白化技术实践上篇

这是第一种完成是非化的办法,接下来介绍别的一种。

1.2、给 View 设置 Paint 完成是非化

View 有个如下 Api :

App 黑白化技术实践上篇

这个办法是用来敞开离屏缓冲的,其接纳两个参数。

第一个参数接纳一个 Int 类型的值,其有三种情况:

1、LAYER_TYPE_NONE:视图正常烘托,不受屏幕外缓冲区支持。这是默许行为。

2、LAYER_TYPE_HARDWARE:假如运用经过硬件加速,视图在硬件中烘托为硬件纹路。假如运用未经过硬件加速,此层类型的行为办法与 LAYER_TYPE_SOFTWARE 相同。

3、LAYER_TYPE_SOFTWARE:运用软件来烘托视图,制作到 Bitmap,并顺便封闭硬件加速 。

第二个参数接纳一个 Paint,也便是画笔,那么咱们就能够对画笔做配置,然后到达是非化的作用。

有了思路,咱们先做一个简略的测验:

1、修正 activity.main.xml 的布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:padding="20dp"
    tools:context=".MainActivity">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="erdai666"
        android:textColor="@android:color/holo_green_light"
        android:textSize="30sp"/>
    <TextView
        android:id="@+id/tvBlackAndWhite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="erdai666"
        android:textColor="@android:color/holo_green_light"
        android:textSize="30sp"/>
    <Button
        android:id="@+id/btnBlackAndWhite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/holo_green_light"
        android:text="erdai666" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="erdai666"
        android:textColor="@android:color/holo_green_light" />
</LinearLayout>

上述布局很简略,便是给要是非化的 TextView ,Button 加了一个 id,方便咱们在 Activity 里边操作

2、修正 MainActivity:

class MainActivity: AppCompatActivity() /*: BaseActivity()*/ {
    //颜色饱和度为 0 的 Paint
    private val paint by lazy {
        val p = Paint()
        val cm = ColorMatrix()
        cm.setSaturation(0f)
        p.colorFilter = ColorMatrixColorFilter(cm)
        p
    }
    //是非化 TextView
    private val tvBlackAndWhite by lazy {
        findViewById<TextView>(R.id.tvBlackAndWhite)
    }
    //是非化 Button
    private val btnBlackAndWhite by lazy {
        findViewById<Button>(R.id.btnBlackAndWhite)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
      	//给 View 设置颜色饱和度为 0 的 Paint 完成是非化
        tvBlackAndWhite.setLayerType(View.LAYER_TYPE_HARDWARE,paint)
        btnBlackAndWhite.setLayerType(View.LAYER_TYPE_HARDWARE,paint)
    }
}

3、运转 app ,作用展现:

App 黑白化技术实践上篇

了解了 App 是非化的原理,接下来咱们就来完成 App 真正的是非化

二、App 是非化计划实践

上述咱们都是对单个 View 进行是非化处理,那有没有什么办法,让整个页面都变成是非化的呢?

答:有的,咱们能够找到当前 View 树一个合适的父 View,对他进行是非化设置或者替换为自定义是非化 View,由于父 View 的 Canvas 和 Paint 是往下分发的,所以它所包括的子 View 都会是非化处理,这样咱们就能够完成 App 是非化

可是我有一些疑问:哪个父 View 是最合适的呢?详细怎么完成呢?

带着上面的疑问,咱们看下下面这张图:

App 黑白化技术实践上篇

每个页面中有一个尖端 View 叫 DecorView,DecorView 中包括一个竖直方向的 LinearLayout,LinearLayout 由两部分组成,第一部分是标题栏,第二部分是内容栏,内容栏是一个 FrameLayout,咱们在 Activity 中调用 setContentView 便是将 View 增加到这个 FrameLayout 中。

了解了上面的内容,你心中是否有了完成计划了呢?

1、是不是能够拿到页面对应的 DecorView ,对其进行是非化设置

2、是不是能够把内容栏(FrameLayout)替换为自定义的 FrameLayout(是非化的 FrameLayout)

上述两种计划都是可行的

2.1、计划一:对 DecorView 进行是非化设置

想要拿到一个页面的 DecorView 有许多办法,主要介绍两种:

1、直接在 Activity 中经过 Window 获取 DecorView

window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE,paint)

Tips: 建议创立一个 Activity 的基类 BaseActivity,在 BaseActivity 里边处理,这样一切承继 BaseActivity 的都会收效

那假如我有些 Activity 没有承继呢?那你接着往下看

2、在 Application 中注册 registerActivityLifecycleCallbacks 回调,回调中经过 activity 实例同样能够拿到 DecorView

registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
                val decorView = activity.window.decorView
                decorView.setLayerType(View.LAYER_TYPE_HARDWARE, paint)
            }
            override fun onActivityStarted(activity: Activity) {
            }
            override fun onActivityResumed(activity: Activity) {
            }
            override fun onActivityPaused(activity: Activity) {
            }
            override fun onActivityStopped(activity: Activity) {
            }
            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
            }
            override fun onActivityDestroyed(activity: Activity) {
            }
        })

这种办法一切的 Activity 都会收效。

看一眼作用:

App 黑白化技术实践上篇

能够看到,整个页面都是非化了。

大家能够思考一下这种计划有什么不足之处?后面在讲

2.2、计划二:替换内容栏 FrameLayout 为是非化 FrameLayout

怎样替换?

这个你就需求对 LayoutInflater 的 inflate 过程有一定的了解,如下办法截图:

App 黑白化技术实践上篇

能够看到,LayoutInflater 在创立 View 的过程中:

1、优先运用 mFactory2 去创立 View ,假如 mFactory2 为空则运用 mFactory,mFactory 为空才会运用 mPrivateFactory

2、Activity 中,体系给咱们设置了 mFactory2:

App 黑白化技术实践上篇

实践流程跟下去最终便是想做如下处理:

将一些体系的 View 替换为:androidx.appcompat.widget 下的 View,如:TextView -> AppCompatTextView ,ImageView -> AppCompatImageView。

3、Activity 能够复写 onCreateView 办法,这个办法其实也是 LayoutFactory 在构建 View 的时分回调出来的,一般对应其内部的 mPrivateFactory。

4、现在体系对于 FrameLayout 并没有特别处理,Activity 能够复写 onCreateView 办法,然后将内容栏 FrameLayout 替换为是非化 FrameLayout 即可。

了解了替换思路,接下来咱们实践一下。

1、创立 BaseActivity ,将替换逻辑写在基类里边:

abstract class BaseActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(getLayoutId())
        initView()
    }
    abstract fun getLayoutId(): Int
    abstract fun initView()
    override fun onCreateView(
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
    ): View? {
        try {
            //tag name 为 FrameLayout
            if ("FrameLayout" == name) {
                val attributeCount = attrs.attributeCount
                for (i in 0 until attributeCount) {
                    //特点名称
                    val attributeName = attrs.getAttributeName(i)
                    //特点值
                    val attributeValue = attrs.getAttributeValue(i)
                    //特点名称为:id
                    if ("id" == attributeName) {
                      	//@16908290 => 16908290
                        val resId = Integer.parseInt(attributeValue.substring(1))
                      	//获取资源名称:android:id/content
                        val idValue = resources.getResourceName(resId)
                        if ("android:id/content" == idValue) {
                            //假如是 DecorView 的 FrameLayout,替换为 GrayFrameLayout
                            val grayFrameLayout = GrayFrameLayout(context, attrs)
                            return grayFrameLayout
                        }
                    }
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return super.onCreateView(parent, name, context, attrs)
    }
}

在看一眼 GrayFrameLayout:

class GrayFrameLayout(context: Context, attrs: AttributeSet): FrameLayout(context,attrs) {
    //颜色饱和度为 0 的 Paint
    private val paint by lazy {
        val p = Paint()
        val cm = ColorMatrix()
        cm.setSaturation(0f)
        p.colorFilter = ColorMatrixColorFilter(cm)
        p
    }
    override fun draw(canvas: Canvas?) {
        canvas?.saveLayer(null,paint,Canvas.ALL_SAVE_FLAG)
        super.draw(canvas)
        canvas?.restore()
    }
    //分发给子 View
    override fun dispatchDraw(canvas: Canvas?) {
        canvas?.saveLayer(null,paint,Canvas.ALL_SAVE_FLAG)
        super.dispatchDraw(canvas)
        canvas?.restore()
    }
}

2、修正一下 MainActivity

class MainActivity: BaseActivity() {
    override fun getLayoutId(): Int {
        return R.layout.activity_main
    }
    override fun initView() {
    }
}

3、最终运转 App ,看一眼作用:

App 黑白化技术实践上篇

状态栏颜色没变,,手动设置状态栏颜色和标题栏颜色保持一致

汲取标题栏颜色值:#4A4A4A ,在 BaseActivity 里边设置一下:

abstract class BaseActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
      	//5.0及以上才干设置状态栏颜色
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            window.statusBarColor = Color.parseColor("#4A4A4A")
        }
        setContentView(getLayoutId())
        initView()
    }
    //...
}

运转 App,在看一眼作用:

App 黑白化技术实践上篇

ok,现在整个页面都是非化了,。

三、问题

3.1、计划一问题

接下来咱们看看计划一存在的不足之处,咱们在第一个 Button 按钮增加点击事情,让它弹出一个 Dialog:

//1、activity_main.xml,给第一个 Button 增加点击事情
<Button
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:onClick="btnClick"
   android:text="erdai666"
   android:textColor="@android:color/holo_green_light" />
//2、MainActivity
fun btnClick(view: View) {
    AlertDialog.Builder(this)
        .setTitle("标题")
        .setMessage("owejfioweofwe")
        .setPositiveButton("确认"){dialog,which->
            dialog.dismiss()
         }
         .setNegativeButton("取消"){dialog,which->
             dialog.dismiss()
         }
         .show()
    }

作用展现:

App 黑白化技术实践上篇

Dialog 并未是非化,为啥呢?先记着

3.2、计划二问题

把上述代码放在计划二跑一遍,你会发现 Dialog 是非化了,如下图:

App 黑白化技术实践上篇

可是假如咱们换成 PopupWindow:

//1、activity_main.xml,给第二个 Button 增加点击事情
<Button
   android:id="@+id/btnBlackAndWhite"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:onClick="btnClick1"
   android:text="erdai666"
   android:textColor="@android:color/holo_green_light" />
//2、popup_window_view.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/color_E62117"
        android:orientation="vertical">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="10dp"
            android:text="function1"
            android:textColor="@color/white"
            android:textSize="20sp" />
        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:background="@color/white" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="10dp"
            android:text="function2"
            android:textColor="@color/white"
            android:textSize="20sp" />
    </LinearLayout>
</FrameLayout>
//3、MainActivity
fun btnClick1(view: View) {
  	val contentView = layoutInflater.inflate(R.layout.popup_window_view,null)
        val popupWindow = PopupWindow(
        contentView,
        ViewGroup.LayoutParams.WRAP_CONTENT,
        ViewGroup.LayoutParams.WRAP_CONTENT,
        true
    )
    popupWindow.isOutsideTouchable = true
    popupWindow.isTouchable = true
    popupWindow.setBackgroundDrawable(ColorDrawable())
    popupWindow.showAsDropDown(view)
}

作用展现:

App 黑白化技术实践上篇

PopupWindow 没有是非化。

整理一下计划一和计划二的问题:

1、计划一:Dialog,PopupWindow 都未是非化

2、计划二:Dialog 是非化,PopupWindow 未是非化

小朋友,你是不是有许多问号?为啥呢?

想了解这些问题,咱们首要得对 Android 的 Window 机制,Dialog 源码,PopupWindow 源码有一定的了解,引荐一篇文章:Android全面解析之Window机制 ,这儿就不展开讲了

计划一之所以 Dialog,PopupWindow 都未是非化,是由于 Activity,Dialog,PopupWindow 它们具有不同的 DecorView ,你设置 Activity 的 DecorView,当然不会影响 Dialog,PopupWindow

计划二之所以 Dialog 是非化,PopupWindow 未是非化,是由于 Dialog 和 Activity 具有相同的 View 结构,如下图:

App 黑白化技术实践上篇

Dialog 创立了新的 PhoneWindow,运用了 PhoneWindow 的 DecorView 模板。而 PopupWindow 并没有。

两种计划都不可,问题到了这儿似乎无解了,真的无解了吗?

3.2、新思路

想一下,Activity,Dialog,PopupWindow 或其他一些 Window 组件它们是不是都要进行 Window 的增加, Window 的增加最终会走到如下办法:

//WindowManagerGlobal#addView
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
        //...
	synchronized (mLock) {
        // 将 view 增加到 mViews,mViews 是一个 ArrayList 集合
        mViews.add(view);
        // 最终经过 viewRootImpl 来增加 window
        try {
            root.setView(view, wparams, panelParentView);
        } 
    }  
}

Tips:

Window 是 View 树的载体,View 树是 Window 的详细表现形式,View 树能够是一个单独的 View,也能够是许多 View 组合。

就比如一个班级,班级是学生的载体,学生是班级的详细表现

WindowManagerGlobal 是一个全局单例,其中 mViews 是一个集合,App 中一切的 Window 在增加的时分都会被它给存起来。

那咱们是不是能够 Hook 拿到 mViews 中一切的 View 然后对他们进行是非化设置,这样是不是一切的页面都变成是非化了呢?

限于篇幅,我打算在写一篇文章去对新思路进行实践,

四、总结

本篇文章咱们介绍了:

1、App 是非化完成原理:将 Paint 的饱和度设置为 0,然后进行 View 的制作

2、App 是非化两种计划实践:

1、对页面的 DecorView 进行是非化设置

2、替换页面的内容栏 FramLaout 为是非化 FrameLayout

3、分析了 App 是非化两种计划存在的一些问题

1、计划一:Dialog,PopupWindow 是非化不收效

2、计划二:Dialog 是非化收效,PopupWindow 是非化不收效

4、给出了 App 是非化两种计划出现问题的原因以及新的思路

关于新思路实践,预知后事怎么,且听下回分解。

好了,本篇文章到这儿就完毕了,希望能给你带来协助

感谢你阅览这篇文章

参考和引荐

App 是非化完成探索,有一行代码完成的计划吗?

Android全面解析之Window机制

App全局灰度化实践-GlobalGray

你的点赞,评论,是对我巨大的鼓舞!

欢迎关注我的大众号: sweetying ,文章更新可第一时间收到

假如有问题,大众号内有加我微信的进口,在技能学习、个人成长的道路上,咱们一起行进!