场景:当启动页处于倒计时阶段,用户将其切换为后台的多任务卡片状态,倒计时会继续执行,直到最后执行相关逻辑(一般会跳转引导页、进入主页等)
期望:而综合市场来看,一般我们期望的是当其处于多卡片状态,需要暂停倒计时,只有恢复前台状态后继续计时。而不是重新计时或已计时完毕
关于启动页的一些基础内容,之前已经做过总结了,此篇主要用于解决上方提到的业务场景
- Android进阶之路 - Splash、Welcome欢迎页面简单实现(当年入门时候写的,Demo级入门示例)
- Android进阶之路 - 快速实现启动页(基础版,可用于实战项目)
业务实战
- 项目实战
- AI提供方案
- CountDownTimer + 生命周期感知
- ViewModel + LiveData
项目实战
以下是我从项目中剥离的伪代码,主要用于解决不同生命周期,计时器带来的影响,核心思想有以下几点
- 倒计时长根据当前计时器的变化而实时变更
- 当处于
onPause
(后台)时,取消计时器 - 当处于
onResume
(前台)时,将计时器剩余时长传入计时器中
因为我们不考虑横竖屏切换场景,所以在 AndroidMainfest
中直接为启动页 Activityandroid:screenOrientation="portrait"
<activityandroid: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.xxxximport 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 = millisUntilFinishedmainHandler.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"><ImageViewandroid: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" /><RelativeLayoutandroid: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"><TextViewandroid: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? = nullprivate 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 = millisUntilFinishedval seconds = millisUntilFinished / 1000textView_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 架构组件
- 组件化之路 - Lifecycle一知半解
- 组件化之路 - LiveData一知半解
- 组件化之路 - ViewModel一知半解
- 组件化之路 - LiveData + ViewModel一知半解
优势
- 生命周期感知:
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.launchclass CountdownViewModel : ViewModel() {private val _remainingTime = MutableLiveData<Long>()val remainingTime: LiveData<Long> = _remainingTimeprivate val _countdownFinished = MutableLiveData<Boolean>()val countdownFinished: LiveData<Boolean> = _countdownFinishedprivate var countdownJob: Job? = nullprivate var initialDuration: Long = 0Lfun startCountdown(duration: Long) {initialDuration = duration_remainingTime.value = duration_countdownFinished.value = falsecountdownJob?.cancel() // 取消之前的任务countdownJob = viewModelScope.launch {var timeLeft = durationwhile (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 ?: initialDurationoverride fun onCleared() {super.onCleared()pauseCountdown()}
}
在 Activity/Fragment 中使用 ViewModel
class CountdownActivity : AppCompatActivity() {private lateinit var viewModel: CountdownViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_countdown)// 初始化ViewModelviewModel = ViewModelProvider(this).get(CountdownViewModel::class.java)// 观察剩余时间并更新UIviewModel.remainingTime.observe(this) { timeMillis ->val seconds = timeMillis / 1000textView_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)}}
}