一、需求描述
我们知道在微博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
}
}