Android实战进阶 - 启动页

场景:当启动页处于倒计时阶段,用户将其切换为后台的多任务卡片状态,倒计时会继续执行,直到最后执行相关逻辑(一般会跳转引导页、进入主页等)
期望:而综合市场来看,一般我们期望的是当其处于多卡片状态,需要暂停倒计时,只有恢复前台状态后继续计时。而不是重新计时或已计时完毕

关于启动页的一些基础内容,之前已经做过总结了,此篇主要用于解决上方提到的业务场景

业务实战

    • 项目实战
    • AI提供方案
      • [CountDownTimer + 生命周期感知](#CountDownTimer + 生命周期感知)
      • [ViewModel + LiveData](#ViewModel + LiveData)

项目实战

以下是我从项目中剥离的伪代码,主要用于解决不同生命周期,计时器带来的影响,核心思想有以下几点

  • 倒计时长根据当前计时器的变化而实时变更
  • 当处于onPause(后台)时,取消计时器
  • 当处于onResume(前台)时,将计时器剩余时长传入计时器中

因为我们不考虑横竖屏切换场景 ,所以在 AndroidMainfest 中直接为启动页 Activityandroid:screenOrientation="portrait"

xml 复制代码
   <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>

实现方式

kotlin 复制代码
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 复制代码
<?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 + 生命周期感知

考虑到了横竖屏场景,兼容场景更多一些

核心要点

  1. onPause()中取消计时器:阻止它在后台触发 onFinish()跳转。
  2. 保存剩余时间:在 onTick()中持续更新 remainingTimeInMillis变量。
  3. onResume()中恢复计时:根据保存的剩余时间重新开始计时。如果时间已到,直接跳转。
  4. 处理配置变更:通过 onSaveInstanceState保存数据,防止屏幕旋转等问题。
kotlin 复制代码
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

kotlin 复制代码
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

kotlin 复制代码
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)
        }
    }
}
相关推荐
杉氧1 分钟前
状态管理变迁史:为什么我们放弃了 Redux 选择 Zustand?
android·前端·react native
三少爷的鞋4 小时前
别再靠 Code Review 守底线:我做了一个静态分析项目 RedLine
android
y = xⁿ12 小时前
DeepSeek Harness 学习日记:关于Agent接口,工具调用的底层实现
android·java·学习
2601_9620664914 小时前
【Sql Server】Update中的From语句,以及常见更新操作方式
android·java·数据库
用户693717500138419 小时前
曾经安卓开发人手一个的 EventBus,为什么现在没人爱用了?
android·android studio
九皇叔叔21 小时前
MySQL ORDER BY 性能优化详解:Using filesort、联合索引、ASC/DESC 与覆盖索引
android·adb
OriginCoding1 天前
用 AI Agent 协作完成一个 Android TOTP 应用:从需求边界到 v1.0.0
android·ai编程
杉氧1 天前
React 灵魂:深入剖析 Hooks 与副作用(Side Effects)陷阱
android·前端·react native
又见情义1 天前
RK3568 + RTL8211F 网络唤醒(WOL)功能适配全记录
android·网络·驱动开发
用户0934077735141 天前
HarmonyOS WPS Open SDK 实践:registerApp 鉴权与就绪门禁
android·typescript·harmonyos