android 仿微博护眼模式

一、需求描述

我们知道在微博APP的设置中有一个护眼模式的开关选项,说是护眼模式,当我们打开微博此功能时,就会发现“咦,这不是在页面上添加了一个透明度的view吗?”,接下来我们带着疑问开始对此功能进行实现。

二、实现方案

通过仔细观察发现确实就是在每个activity页面的上层添加了一个有透明度的view,且需要满以下要求:

1、需要在页面的最上层创建一个view,不能被其他view所遮挡。

2、不能影响下层view的触摸事件。

3、是一个全屏的透明度view。

4、打开和关闭后需要立即生效。

首先第一点我们知道activity的视图层级PhoneWindow是在最上层,所以我们可以在window中添加view,伪代码:
activity.windowManager.addView(eyeCareView, eyeCareViewParam)
这样就创建在了activity的最上层,那么不能影响下层view的触摸事件和全屏怎么设置呢?
伪代码:

val eyeCareViewParam = WindowManager.LayoutParams(
    WindowManager.LayoutParams.TYPE_APPLICATION,
    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
            or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
            or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
            or WindowManager.LayoutParams.FLAG_FULLSCREEN
            or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
    PixelFormat.TRANSPARENT
)

eyeCareViewParam.gravity = Gravity.TOP or Gravity.START

activity.windowManager.defaultDisplay.apply {
    val point = Point()
    this.getRealSize(point)
    eyeCareViewParam.width = point.x
    eyeCareViewParam.height = point.y
}

view的创建和添加的问题解决了,接下来就是什么打开和关闭立即生效的问题,首页在设置页打开和关闭就很好解决,我们只要在相对应的点击事件下添加view和移除view即可,伪代码:

findViewById<RelativeLayout>(R.id.rl_open_eye).setOnClickListener {
    openEye()
    Toast.makeText(this,"已开启护眼模式",Toast.LENGTH_SHORT).show()
}

findViewById<RelativeLayout>(R.id.rl_close_eye).setOnClickListener {
    closeEye()
    Toast.makeText(this,"已关闭护眼模式",Toast.LENGTH_SHORT).show()
}

那么,在设置页面返回上一页或者跳转到另一个页面怎么生效呢?也很简单,只要我们在设置开关时本地存储开关状态(这里的存储方法有很多例如SharedPreferences、jetpack的 DataStore等),在acivity(通常情况下我们会创建一个基类)的onStart()生命周期中根据开关状态进行添加和移除的操作,伪代码:

@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart(owner: LifecycleOwner?) {
    if (isOpenEye()) {
        //打开护眼模式
        openEye()
    } else {
        //关闭护眼模式
        closeEye()
    }
}

这样,功能实现基本就完成了。不过等一下,我们是不是可以增加一个在对应时间段自动切换的模式?,这样就不需要手动去打开和关闭了。那么,我们就需要在存储开关状态的基础上再增加一个自动切换的状态,伪代码:

/**
 * 护眼模式关闭状态
 */
const val eyeCloseType = 0

/**
 * 护眼模式打开状态
 */
const val eyeOpenType = 1

/**
 * 护眼模式自动切换状态
 */
const val eyeSwitchType = 2

当然,我们还需要判断当前时间是否在打开的时间段内,伪代码:

**
 * 判断当前系统时间是否在指定时间的范围内
 * [beginHour] 开始小时,例如22
 * [beginMin]  开始小时的分钟数,例如30
 * [endHour]   结束小时,例如 8
 * [endMin]    结束小时的分钟数,例如0
 * @return true表示在范围内, 否则false
 */
private fun isCurrentInTimeScope(): Boolean {
    var result: Boolean
    val aDayInMillis = (1000 * 60 * 60 * 24).toLong()
    val currentTimeMillis = System.currentTimeMillis()
    val now = Time()
    now.set(currentTimeMillis)
    val startTime = Time()
    startTime.set(currentTimeMillis)
    startTime.hour = beginHour
    startTime.minute = beginMin
    val endTime = Time()
    endTime.set(currentTimeMillis)
    endTime.hour = endHour
    endTime.minute = endMin
    // 跨天的特殊情况(比如22:00-8:00)
    if (!startTime.before(endTime)) {
        startTime.set(startTime.toMillis(true) - aDayInMillis)
        result = !now.before(startTime) && !now.after(endTime)
        val startTimeInThisDay = Time()
        startTimeInThisDay.set(startTime.toMillis(true) + aDayInMillis)
        if (!now.before(startTimeInThisDay)) {
            result = true
        }
    } else {
        //普通情况(比如 8:00 - 14:00)
        result = !now.before(startTime) && !now.after(endTime)
    }
    return result
}

这样,完整的功能就实现完了。

三、完整代码

这里view的创建以及管理是封装在了一个帮助类中,代码:

/**
 * 护眼模式帮助类
 * @author xiaoman
 */
class CreateEyeCareViewHelper(private val activity: Activity, lifecycle: Lifecycle? = null) :
    LifecycleObserver {
    private var eyeCareView: FrameLayout? = null

    companion object {
        /**
         * 护眼模式关闭状态
         */
        const val eyeCloseType = 0

        /**
         * 护眼模式打开状态
         */
        const val eyeOpenType = 1

        /**
         * 护眼模式自动切换状态
         */
        const val eyeSwitchType = 2

        /**
         * 自动切换模式开始小时
         */
        const val beginHour = 20

        /**
         * 自动切换模式开始分钟
         */
        const val beginMin = 0

        /**
         * 自动切换模式结束小时
         */
        const val endHour = 6

        /**
         * 自动切换模式结束分钟
         */
        const val endMin = 0
    }

    init {
        lifecycle?.addObserver(this)
    }

    /**
     * 添加护眼模式浮层
     */
    private fun createEyeView() {
        if (eyeCareView != null) {
            return
        }
        eyeCareView = FrameLayout(activity)
        if (!isOpenEye()) {
            closeEye()
            return
        }
        val eyeCareViewParam = WindowManager.LayoutParams(
            WindowManager.LayoutParams.TYPE_APPLICATION,
            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                    or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                    or WindowManager.LayoutParams.FLAG_FULLSCREEN
                    or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
            PixelFormat.TRANSPARENT
        )

        eyeCareViewParam.gravity = Gravity.TOP or Gravity.START

        activity.windowManager.defaultDisplay.apply {
            val point = Point()
            this.getRealSize(point)
            eyeCareViewParam.width = point.x
            eyeCareViewParam.height = point.y
        }
        activity.windowManager.addView(eyeCareView, eyeCareViewParam)
        openEye()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun onStart(owner: LifecycleOwner?) {
        if (isOpenEye()) {
            //打开护眼模式
            openEye()
        } else {
            //关闭护眼模式
            closeEye()
        }
    }

    /**
     * 开启护眼模式
     */
    fun openEye() {
        if (eyeCareView != null) {
            if (isOpenEye()) {
                eyeCareView!!.setBackgroundColor(Color.parseColor("#4D000000"))
            } else {
                closeEye()
            }
        } else {
            createEyeView()
        }
    }


    /**
     * 关闭护眼模式
     */
    fun closeEye() {
        eyeCareView?.let {
            it.setBackgroundColor(Color.TRANSPARENT)
            if (it.isAttachedToWindow) {
                activity.windowManager.removeView(it)
            }
            eyeCareView = null
        }
    }

    /**
     * 判断当前系统时间是否在指定时间的范围内
     * [beginHour] 开始小时,例如22
     * [beginMin]  开始小时的分钟数,例如30
     * [endHour]   结束小时,例如 8
     * [endMin]    结束小时的分钟数,例如0
     * @return true表示在范围内, 否则false
     */
    private fun isCurrentInTimeScope(): Boolean {
        var result: Boolean
        val aDayInMillis = (1000 * 60 * 60 * 24).toLong()
        val currentTimeMillis = System.currentTimeMillis()
        val now = Time()
        now.set(currentTimeMillis)
        val startTime = Time()
        startTime.set(currentTimeMillis)
        startTime.hour = beginHour
        startTime.minute = beginMin
        val endTime = Time()
        endTime.set(currentTimeMillis)
        endTime.hour = endHour
        endTime.minute = endMin
        // 跨天的特殊情况(比如22:00-8:00)
        if (!startTime.before(endTime)) {
            startTime.set(startTime.toMillis(true) - aDayInMillis)
            result = !now.before(startTime) && !now.after(endTime)
            val startTimeInThisDay = Time()
            startTimeInThisDay.set(startTime.toMillis(true) + aDayInMillis)
            if (!now.before(startTimeInThisDay)) {
                result = true
            }
        } else {
            //普通情况(比如 8:00 - 14:00)
            result = !now.before(startTime) && !now.after(endTime)
        }
        return result
    }

    /**
     * 护眼模式是否打开
     */
    private fun isOpenEye(): Boolean {
        return SpUtil.getInstance(activity)
            .getIntValue(SpUtil.EYE_CARE_STYLE) == eyeOpenType || isSwitchEye()
    }

    /**
     * 护眼模式是否关闭
     */
    private fun isCloseEye(): Boolean {
        return SpUtil.getInstance(activity)
            .getIntValue(SpUtil.EYE_CARE_STYLE) == eyeCloseType || !isSwitchEye()
    }

    /**
     * 护眼模式自动切换状态是否在打开时间阶段
     */
    private fun isSwitchEye(): Boolean {
        return SpUtil.getInstance(activity)
            .getIntValue(SpUtil.EYE_CARE_STYLE) == eyeSwitchType && isCurrentInTimeScope()
    }



}

然后只要在activity的基类中初始化帮助类,代码:

abstract class BaseActivity : AppCompatActivity(){

    protected var createEyeCareViewHelper: CreateEyeCareViewHelper? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setLayout()
        if (canOpenEye()) {
            createEyeCareViewHelper = CreateEyeCareViewHelper(this, lifecycle)
        }
        initView(savedInstanceState)

    }

    private fun setLayout() {
        setContentView(getLayoutId())
    }

    @LayoutRes
    abstract fun getLayoutId(): Int

    abstract fun initView(savedInstanceState: Bundle?)


    /**
     * @return 是否需要开启护眼模式 默认开启
     */
    protected fun canOpenEye(): Boolean {
        return true
    }

}

当然还有在设置页面进行对应状态的操作,代码:

class SettingActivity : BaseActivity() {
    private lateinit var ivOpenEye :ImageView
    private lateinit var ivCloseEye :ImageView
    private lateinit var ivSwitchEye :ImageView

    override fun getLayoutId(): Int = R.layout.activity_setting

    override fun initView(savedInstanceState: Bundle?) {
        ivOpenEye = findViewById(R.id.iv_open_eye)
        ivCloseEye = findViewById(R.id.iv_close_eye)
        ivSwitchEye = findViewById(R.id.iv_switch_eye)

        var type = SpUtil.getInstance(this).getIntValue(SpUtil.EYE_CARE_STYLE)
        when(type){
            //开启
            CreateEyeCareViewHelper.eyeOpenType ->{
                setOenStyle()
            }
            //关闭
            CreateEyeCareViewHelper.eyeCloseType ->{
                setCloseStyle()
            }
            //自动开启
            CreateEyeCareViewHelper.eyeSwitchType ->{
                setSwitchStyle()
            }

        }

        findViewById<ImageView>(R.id.ivBack).setOnClickListener {
            finish()
        }



        findViewById<RelativeLayout>(R.id.rl_open_eye).setOnClickListener {
            if (type == CreateEyeCareViewHelper.eyeOpenType){
                return@setOnClickListener
            }
            setOenStyle()
            SpUtil.getInstance(this).setIntValue(SpUtil.EYE_CARE_STYLE, CreateEyeCareViewHelper.eyeOpenType)
            type = CreateEyeCareViewHelper.eyeOpenType
            createEyeCareViewHelper?.openEye()
            Toast.makeText(this,"已开启护眼模式",Toast.LENGTH_SHORT).show()
        }

        findViewById<RelativeLayout>(R.id.rl_close_eye).setOnClickListener {
            if (type == CreateEyeCareViewHelper.eyeCloseType){
                return@setOnClickListener
            }
            setCloseStyle()
            SpUtil.getInstance(this).setIntValue(SpUtil.EYE_CARE_STYLE, CreateEyeCareViewHelper.eyeCloseType)
            type = CreateEyeCareViewHelper.eyeCloseType
            createEyeCareViewHelper?.closeEye()
            Toast.makeText(this,"已关闭护眼模式",Toast.LENGTH_SHORT).show()
        }

        findViewById<RelativeLayout>(R.id.rl_switch_eye).setOnClickListener {
            if (type == CreateEyeCareViewHelper.eyeSwitchType){
                return@setOnClickListener
            }
            setSwitchStyle()
            SpUtil.getInstance(this).setIntValue(SpUtil.EYE_CARE_STYLE, CreateEyeCareViewHelper.eyeSwitchType)
            type = CreateEyeCareViewHelper.eyeSwitchType
            createEyeCareViewHelper?.openEye()
            Toast.makeText(this,"已开启自动切换模式",Toast.LENGTH_SHORT).show()
        }
    }

    /**
     * 开启样式
     */
    private fun setOenStyle() {
        ivOpenEye.visibility = View.VISIBLE
        ivCloseEye.visibility = View.GONE
        ivSwitchEye.visibility = View.GONE
    }

    /**
     * 关闭样式
     */
    private fun setCloseStyle() {
        ivOpenEye.visibility = View.GONE
        ivCloseEye.visibility = View.VISIBLE
        ivSwitchEye.visibility = View.GONE
    }

    /**
     * 自动开启样式
     */
    private fun setSwitchStyle() {
        ivOpenEye.visibility = View.GONE
        ivCloseEye.visibility = View.GONE
        ivSwitchEye.visibility = View.VISIBLE
    }
}

四、实现效果

SM-G9500_20211214224222.gif

五、获取源码

github地址:https://github.com/still-soul/EyeProtectionMode

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341

推荐阅读更多精彩内容