Android实战进阶 - 启动页

发布于:2025-09-10 ⋅ 阅读:(17) ⋅ 点赞:(0)

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

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

项目实战

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

  • 倒计时长根据当前计时器的变化而实时变更
  • 当处于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 + 生命周期感知

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

核心要点

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

网站公告

今日签到

点亮在社区的每一天
去签到