场景:当启动页处于倒计时阶段,用户将其切换为后台的多任务卡片状态,倒计时会继续执行,直到最后执行相关逻辑(一般会跳转引导页、进入主页等)
期望:而综合市场来看,一般我们期望的是当其处于多卡片状态,需要暂停倒计时,只有恢复前台状态后继续计时。而不是重新计时或已计时完毕
关于启动页的一些基础内容,之前已经做过总结了,此篇主要用于解决上方提到的业务场景
项目实战
以下是我从项目中剥离的伪代码,主要用于解决不同生命周期,计时器带来的影响,核心思想有以下几点
- 倒计时长根据当前计时器的变化而实时变更
- 当处于
onPause
(后台)时,取消计时器 - 当处于
onResume
(前台)时,将计时器剩余时长传入计时器中
因为我们不考虑横竖屏切换场景,所以在 AndroidMainfest
中直接为启动页 Activityandroid:screenOrientation="portrait"
<activity
android:name=".loading.SplashActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
实现方式
package cn.xxxx
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.CountDownTimer
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.*
import me.jessyan.autosize.internal.CancelAdapt
import timber.log.Timber
import javax.inject.Inject
@SuppressLint("CustomSplashScreen")
@AndroidEntryPoint
class SplashActivity : AppCompatActivity(), CancelAdapt {
lateinit var tvJump: TextView
// 倒计时长
var remainingTimeInMillis: Long? = null
//计时器
private var countDownTimer: CountDownTimer? = null
@SuppressLint("SourceLockedOrientationActivity", "MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash_l)
//右上角跳过的视图
tvJump = findViewById<TextView>(R.id.tv_jump)
//初始化为计时器时间
remainingTimeInMillis = 5 * 1000
//跳过逻辑
tvJump.onClick {
countDownTimer?.cancel()
next()
}
}
override fun onResume() {
super.onResume()
//之所以写是因为在项目里广告时间是后台返回,如果只是单纯固定时长可去除该判断
if (!remainingTimeInMillis.isNull()) {
// Activity回到前台时,检查剩余时间
if ((remainingTimeInMillis ?: 0) <= 0) {
// 如果时间已经耗尽,直接跳转
next()
} else {
// 如果时间还有剩余,重新启动一个计时器,从剩余时间开始
startTimer(remainingTimeInMillis ?: 0)
}
}
}
override fun onPause() {
super.onPause()
// Activity进入后台时,立即取消计时器(防止onFinish在后台被调用),remainingTimeInMillis还保存着最新的剩余时间
countDownTimer?.cancel()
}
//计时器
private fun startTimer(time: Long) {
countDownTimer?.cancel()
countDownTimer = object : CountDownTimer(time, 1000) {
override fun onTick(millisUntilFinished: Long) {
//实时更新剩余的倒计时长
remainingTimeInMillis = millisUntilFinished
mainHandler.post { tvJump.text = "${millisUntilFinished / 1000 + 1}s跳过" }
}
override fun onFinish() {
//倒计时结束,进入对应逻辑
next()
}
}.start()
}
override fun onDestroy() {
super.onDestroy()
countDownTimer?.cancel()
countDownTimer = null
}
private fun next() {
//可自行根据业务场景,决定跳转逻辑
// 以下为项目伪代码:判断是否首次登录,运行过帮助引导
val landingVersion = SPUtils.AppSP().get(ConstValue.VER_IS_THIS_VERSION_OPEN_BEFORE, 0) as? Int?
if ((landingVersion ?: 0) >= 4) {
RouterPath.APP_MAIN_ACT //首页
} else {
RouterPath.MAIN_LANDING_ACT //引导页
}.also {
startRouterAndFinish(it) { putBoolean("firstStart", true) }
}
}
}
activity_splash_l
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:background="@color/white"
android:orientation="vertical"
tools:ignore="MissingDefaultResource">
<ImageView
android:id="@+id/background_type_1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/drawable_app_launch"
android:visibility="visible" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginTop="62dp"
android:layout_marginEnd="15dp"
android:background="@drawable/shape_splash_btn_jump_bg"
android:paddingHorizontal="12dp"
android:paddingVertical="4dp">
<TextView
android:id="@+id/tv_jump"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="跳过"
android:textColor="#FFFFFF"
android:textSize="12dp" />
</RelativeLayout>
</FrameLayout>
AI提供方案
自从使用AI后,觉得很多基础性的知识没有了记Blog的必要,更多的可能还行记录项目中遇到的问题
CountDownTimer + 生命周期感知
考虑到了横竖屏场景,兼容场景更多一些
核心要点
- 在
onPause()
中取消计时器:阻止它在后台触发onFinish()
跳转。 - 保存剩余时间:在
onTick()
中持续更新remainingTimeInMillis
变量。 - 在
onResume()
中恢复计时:根据保存的剩余时间重新开始计时。如果时间已到,直接跳转。 - 处理配置变更:通过
onSaveInstanceState
保存数据,防止屏幕旋转等问题。
class CountdownActivity : AppCompatActivity() {
private var countDownTimer: CountDownTimer? = null
private var remainingTimeInMillis: Long = 10000 // 总计时时间,例如10秒
private val totalTimeInMillis: Long = 10000 // 保存总时间用于恢复
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_countdown)
// 从保存的状态中恢复时间(防止旋转屏幕等配置变更)
if (savedInstanceState != null) {
remainingTimeInMillis = savedInstanceState.getLong("REMAINING_TIME", totalTimeInMillis)
}
startCountdown(remainingTimeInMillis)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// 保存当前剩余时间,防止配置变更(如屏幕旋转)导致时间重置
outState.putLong("REMAINING_TIME", remainingTimeInMillis)
}
private fun startCountdown(millisInFuture: Long) {
// 每次启动新计时器前,取消旧的
countDownTimer?.cancel()
countDownTimer = object : CountDownTimer(millisInFuture, 1000) {
override fun onTick(millisUntilFinished: Long) {
// 更新UI,显示剩余时间
remainingTimeInMillis = millisUntilFinished
val seconds = millisUntilFinished / 1000
textView_countdown.text = "剩余时间: ${seconds}秒"
}
override fun onFinish() {
// 只有在Activity处于前台时,才执行跳转逻辑
proceedToNextStep()
}
}.start()
}
private fun proceedToNextStep() {
// 执行你的下一步操作,例如跳转页面
val intent = Intent(this, NextActivity::class.java)
startActivity(intent)
finish()
}
override fun onPause() {
super.onPause()
// Activity进入后台时,立即取消计时器(防止onFinish在后台被调用)
countDownTimer?.cancel()
// 注意:这里我们只是取消了计时器,并没有改变remainingTimeInMillis的值
// 所以remainingTimeInMillis还保存着最新的剩余时间
}
override fun onResume() {
super.onResume()
// Activity回到前台时,检查剩余时间
if (remainingTimeInMillis <= 0) {
// 如果时间已经耗尽,直接跳转
proceedToNextStep()
} else {
// 如果时间还有剩余,重新启动一个计时器,从剩余时间开始
startCountdown(remainingTimeInMillis)
}
}
override fun onDestroy() {
super.onDestroy()
// 彻底销毁Activity时,释放计时器资源
countDownTimer?.cancel()
}
}
ViewModel + LiveData
符合当下主流框架、组件,适用性、兼容性高,但是对于未使用过的朋友,需要一点时间学下组件
Android Architecture Components 架构组件
优势
- 生命周期感知:
ViewModel
独立于UI生命周期,配置变更时数据不会丢失。 - 关注点分离:计时逻辑在
ViewModel
中,UI控制只在Activity中。 - 更健壮:使用
Coroutines
处理后台任务,更加现代和安全。
创建 ViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class CountdownViewModel : ViewModel() {
private val _remainingTime = MutableLiveData<Long>()
val remainingTime: LiveData<Long> = _remainingTime
private val _countdownFinished = MutableLiveData<Boolean>()
val countdownFinished: LiveData<Boolean> = _countdownFinished
private var countdownJob: Job? = null
private var initialDuration: Long = 0L
fun startCountdown(duration: Long) {
initialDuration = duration
_remainingTime.value = duration
_countdownFinished.value = false
countdownJob?.cancel() // 取消之前的任务
countdownJob = viewModelScope.launch {
var timeLeft = duration
while (timeLeft > 0 && isActive) {
delay(1000)
timeLeft -= 1000
_remainingTime.postValue(timeLeft) // 使用postValue确保在主线程更新
}
if (isActive && timeLeft <= 0) {
_countdownFinished.postValue(true)
}
}
}
fun pauseCountdown() {
countdownJob?.cancel()
}
// 获取当前剩余时间,用于在UI层判断
fun getCurrentTime(): Long = _remainingTime.value ?: initialDuration
override fun onCleared() {
super.onCleared()
pauseCountdown()
}
}
在 Activity/Fragment 中使用 ViewModel
class CountdownActivity : AppCompatActivity() {
private lateinit var viewModel: CountdownViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_countdown)
// 初始化ViewModel
viewModel = ViewModelProvider(this).get(CountdownViewModel::class.java)
// 观察剩余时间并更新UI
viewModel.remainingTime.observe(this) { timeMillis ->
val seconds = timeMillis / 1000
textView_countdown.text = "剩余时间: ${seconds}秒"
}
// 观察倒计时是否结束
viewModel.countdownFinished.observe(this) { isFinished ->
if (isFinished) {
proceedToNextStep()
}
}
// 如果是第一次创建,开始计时
if (savedInstanceState == null) {
viewModel.startCountdown(10000)
}
}
private fun proceedToNextStep() {
val intent = Intent(this, NextActivity::class.java)
startActivity(intent)
finish()
}
override fun onPause() {
super.onPause()
// 进入后台时暂停计时
viewModel.pauseCountdown()
}
override fun onResume() {
super.onResume()
val currentTime = viewModel.getCurrentTime()
if (currentTime <= 0) {
// 如果ViewModel中记录的时间已经用完,直接跳转
proceedToNextStep()
} else {
// 否则,重新开始计时(从剩余时间开始)
viewModel.startCountdown(currentTime)
}
}
}