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)
        }
    }
}
相关推荐
2601_9498333915 小时前
flutter_for_openharmony口腔护理app实战+预约管理实现
android·javascript·flutter
2603_9494621017 小时前
Flutter for OpenHarmony社团管理App实战:预算管理实现
android·javascript·flutter
王泰虎19 小时前
安卓开发日记,因为JCenter 关闭导致加载不了三方库应该怎么办
android
2601_949543011 天前
Flutter for OpenHarmony垃圾分类指南App实战:主题配置实现
android·flutter
2601_949833391 天前
flutter_for_openharmony口腔护理app实战+知识实现
android·javascript·flutter
晚霞的不甘1 天前
Flutter for OpenHarmony从基础到专业:深度解析新版番茄钟的倒计时优化
android·flutter·ui·正则表达式·前端框架·鸿蒙
鸟儿不吃草1 天前
android的Retrofit请求https://192.168.43.73:8080/报错:Handshake failed
android·retrofit
Minilinux20181 天前
Android音频系列(09)-AudioPolicyManager代码解析
android·音视频·apm·audiopolicy·音频策略
李子红了时1 天前
【无标题】
android
Android系统攻城狮1 天前
Android tinyalsa深度解析之pcm_close调用流程与实战(一百零四)
android·pcm·tinyalsa·音频进阶·音频性能实战·android hal