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)
        }
    }
}
相关推荐
出门吃三碗饭3 小时前
编译器构造:从零手写汇编与反汇编程序(一)
android·汇编
Just_Paranoid3 小时前
【WorkManager】无法在 Direct Boot 模式下初始化
android·jetpack·usermanager·workmanager·directboot
前端小超超3 小时前
如何配置capacitor 打包的安卓app固定竖屏展示?
android·前端·gitee
顾林海4 小时前
探秘Android JVM TI:虚拟机背后的"隐形管家"
android·面试·性能优化
刘大国5 小时前
<android>反编译魔改安卓系统应用并替换
android
恋猫de小郭6 小时前
Flutter Riverpod 3.0 发布,大规模重构下的全新状态管理框架
android·前端·flutter
纤瘦的鲸鱼6 小时前
MySQL慢查询
android·adb
郭庆汝6 小时前
模型部署:(三)安卓端部署Yolov8-v8.2.99目标检测项目全流程记录
android·yolo·目标检测·yolov8
fatiaozhang95276 小时前
中国移动云电脑一体机-创维LB2004_瑞芯微RK3566_2G+32G_开启ADB ROOT安卓固件-方法3
android·xml·adb·电脑·电视盒子·刷机固件