Android 护眼模式功能实现

4,678 阅读2分钟

相较于 iOS,安卓在 Q 之前并没有原生支持护眼模式,所以我们得自己实现一套。

其实网上代码也挺多的,甚至有些公司的产品就是这个功能。

先来讲下实现的思路,不想先听思路的可以直接跳到代码那块,代码也不难。

1. 实现思路

  • Window 上覆盖一个 View,做到跨 App 及状态栏和导航栏整体覆盖
  • View 的背景设置成过滤蓝光的透明色(滤光层)
  • 注意好 Android 系统兼容性问题

2. 代码

创建一个 EyeCareService,该 Service 用来添加 Window 上的滤光层

class EyeCareService : Service() {
    private lateinit var windowManager: WindowManager
    private lateinit var coverLayout: FrameLayout
    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onCreate() {
        super.onCreate()
        windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val params = WindowManager.LayoutParams().apply {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                this.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY or
                        WindowManager.LayoutParams.TYPE_STATUS_BAR
            } else {
                this.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
            }
            this.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
                    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
                    WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
                    WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            this.format = PixelFormat.TRANSLUCENT
        }
        windowManager.defaultDisplay.apply {
            params.gravity = Gravity.START or Gravity.TOP
            val point = Point()
            this.getRealSize(point)
            //params.width = point.x 在华为某些新机型横屏全屏时失效,
            //只覆盖一半,所以增加遮罩层大小,把宽度也设置为y
            params.width = point.y
            params.height = point.y
        }

        coverLayout = FrameLayout(this)
        coverLayout.setBackgroundColor(Utils.getColor(30))
        windowManager.addView(coverLayout, params)
    }

    override fun onDestroy() {
        windowManager.removeViewImmediate(coverLayout)
        super.onDestroy()
    }


}

滤光层设置获取颜色方法的方法,一般传入 30 即可

/**
 * 过滤蓝光
 *
 * @param blueFilterPercent 蓝光过滤比例[10-80]
 */
public static @ColorInt
int getColor(int blueFilterPercent) {
	int realFilter = blueFilterPercent;
	if (realFilter < 10) {
		realFilter = 10;
	} else if (realFilter > 80) {
		realFilter = 80;
	}
	int a = (int) (realFilter / 80f * 180);
	int r = (int) (200 - (realFilter / 80f) * 190);
	int g = (int) (180 - (realFilter / 80f) * 170);
	int b = (int) (60 - realFilter / 80f * 60);
	return Color.argb(a, r, g, b);
}

activity 中调用启动方法

private fun openEyeCareMode() {

    if (Build.VERSION.SDK_INT >= 23) {
        if (Settings.canDrawOverlays(this)) { //有悬浮窗权限开启服务绑定 绑定权限
            val intent = Intent(this, EyeCareService::class.java)
            startService(intent)
        } else { //没有悬浮窗权限,去开启悬浮窗权限
            try {
                val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
                startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    } else { //默认有悬浮窗权限 但是 华为, 小米,oppo等手机会有自己的一套Android6.0以下 会有自己的一套悬浮窗权限管理 也需要做适配
        val intent = Intent(this, EyeCareService::class.java)
        startService(intent)
    }

}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
        if (Build.VERSION.SDK_INT >= 23) {
            if (!Settings.canDrawOverlays(this)) {
                //权限授予失败,无法开启悬浮窗
                return
            } else {
                //权限授予成功
            }//有悬浮窗权限开启服务绑定 绑定权限
        }
        val intent = Intent(this, EyeCareService::class.java)
        startService(intent)
    }
}

3. 实际效果

4. 后话

剩下一些考虑点,在这我就不贴代码了,稍微讲下

  • 需要实现关闭逻辑,startService 时 intent传递下关闭参数
  • 考虑前台服务,防止退入后台过段时Service被系统直接杀死,导致功能失效