广告引导页

需求描述

广告引导页需求描述:

  • 用户打开App显示5秒钟的广告,广告结束后进入App主页面。
  • 广告结束前,用户可以点击跳过广告。
  • 页面销毁时,计时器销毁。

代码示例

AdvertisingManage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* FileName: AdvertisingManage
* Founder: Jiang Houren
* Create Date: 2022/7/25 12:34
* Profile: 广告管理工具类
*/
class AdvertisingManage {
val TAG = "TAG_AdvertisingManage"
// 监听事件
var advertisingManageListener : AdvertisingManageListener ?=null

// 计时器
private var countDownTimer:CountDownTimer?=object : CountDownTimer(5000,1000){
override fun onTick(millisUntilFinished: Long) {
Log.d(TAG,"广告剩余时间${(millisUntilFinished/1000).toInt()}秒")
advertisingManageListener?.timing((millisUntilFinished/1000).toInt())
}

override fun onFinish() {
Log.d(TAG,"广告结束,准备进入主界面")
advertisingManageListener?.enterMainActivity()
}
}

/**
* 开始计时
*/
fun start(){
Log.d(TAG,"开始计时")
countDownTimer?.start()
}

/**
* 停止计时
*/
fun onCancel(){
Log.d(TAG,"停止计时")
countDownTimer?.cancel()
countDownTimer = null
}

/**
* 广告管理接口
*/
interface AdvertisingManageListener{
/**
* 计时
* @param second 秒
*/
fun timing(second:Int)

/**
* 计时结束,进入主界面
*/
fun enterMainActivity()
}
}
AdvertisingActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* FileName: AdvertisingActivity
* Founder: Jiang Houren
* Create Date: 2022/7/25 12:52
* Profile: 广告引导页
*/
class AdvertisingActivity : BaseActivity() {
// 跳过广告按钮
lateinit var btnIngore:Button
// 广告时间
lateinit var tvAdvertisingTime:TextView

private var advertisingManage:AdvertisingManage?=null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_advertising)
btnIngore = findViewById(R.id.btn_ignore) as Button
tvAdvertisingTime = findViewById(R.id.tv_advertising_time) as TextView

advertisingManage = AdvertisingManage()
advertisingManage?.advertisingManageListener = object : AdvertisingManage.AdvertisingManageListener{
override fun timing(second: Int) {
tvAdvertisingTime.text = "广告剩余$second 秒"
}

override fun enterMainActivity() {
MainActivity.actionStart(this@AdvertisingActivity)
finish()

// 日志打印:
// D/>>>>>>>>TAG_BaseActivity: AdvertisingActivity
// D/TAG_AdvertisingManage: 开始计时
// D/TAG_AdvertisingManage: 广告剩余时间4秒
// D/TAG_AdvertisingManage: 广告剩余时间3秒
// D/TAG_AdvertisingManage: 广告剩余时间2秒
// D/TAG_AdvertisingManage: 广告剩余时间1秒
// D/TAG_AdvertisingManage: 广告剩余时间0秒
// D/TAG_AdvertisingManage: 广告结束,准备进入主界面
// D/>>>>>>>>TAG_BaseActivity: MainActivity
// D/TAG_AdvertisingManage: 停止计时
}
}
// 跳过广告点击事件
btnIngore.setOnClickListener{
MainActivity.actionStart(this)
finish()

// 日志打印:
// D/>>>>>>>>TAG_BaseActivity: AdvertisingActivity
// D/TAG_AdvertisingManage: 开始计时
// D/TAG_AdvertisingManage: 广告剩余时间4秒
// D/TAG_AdvertisingManage: 广告剩余时间3秒
// D/>>>>>>>>TAG_BaseActivity: MainActivity
// D/TAG_AdvertisingManage: 停止计时
}
//开始广告
advertisingManage?.start()
}

override fun onDestroy() {
super.onDestroy()
advertisingManage?.onCancel()
}
}
activity_advertising
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">

<Button
android:id="@+id/btn_ignore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:text="跳过广告页"/>

<TextView
android:id="@+id/tv_advertising_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>
AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
<application
...
<activity android:name=".ui.advertising.AdvertisingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.MainActivity"/>
...
</application>

使用Lifecycle优化上面代码:使业务功能与Activity业务逻辑分离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class AdvertisingManage:LifecycleObserver{
val TAG = "TAG_AdvertisingManage"

// 监听事件
var advertisingManageListener : AdvertisingManageListener ?=null

// 计时器
private var countDownTimer:CountDownTimer?=object : CountDownTimer(5000,1000){
override fun onTick(millisUntilFinished: Long) {
Log.d(TAG,"广告剩余时间${(millisUntilFinished/1000).toInt()}秒")
advertisingManageListener?.timing((millisUntilFinished/1000).toInt())
}

override fun onFinish() {
Log.d(TAG,"广告结束,准备进入主界面")
advertisingManageListener?.enterMainActivity()
}
}

/**
* 开始计时
* 添加注解
*/
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun start(){
Log.d(TAG,"开始计时")
countDownTimer?.start()
}

/**
* 停止计时
* 添加注解
*/
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onCancel(){
Log.d(TAG,"停止计时")
countDownTimer?.cancel()
countDownTimer = null
}

/**
* 广告管理接口
*/
interface AdvertisingManageListener{
/**
* 计时
* @param second 秒
*/
fun timing(second:Int)

/**
* 计时结束,进入主界面
*/
fun enterMainActivity()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class AdvertisingActivity : BaseActivity() {

// 跳过广告按钮
lateinit var btnIngore:Button
// 广告时间
lateinit var tvAdvertisingTime:TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_advertising)
btnIngore = findViewById(R.id.btn_ignore) as Button
tvAdvertisingTime = findViewById(R.id.tv_advertising_time) as TextView

val advertisingManage = AdvertisingManage()
lifecycle.addObserver(advertisingManage)

advertisingManage.advertisingManageListener = object : AdvertisingManage.AdvertisingManageListener{
override fun timing(second: Int) {
tvAdvertisingTime.text = "广告剩余$second 秒"
}

override fun enterMainActivity() {
MainActivity.actionStart(this@AdvertisingActivity)
finish()
}
}
// 跳过广告点击事件
btnIngore.setOnClickListener{
MainActivity.actionStart(this)
finish()
}
}
// 删除了生命周期相关代码
}

解决横竖屏切换导致的计时重置

configChanges 属性

屏幕旋转时,横竖屏切换会导致重新执行每个生命周期方法,生命周期的变化取决于 configChanges 属性。

onSaveInstanceState 方法

通过重写 onSaveInstanceState 方法在 Activity 被销毁时将计时的节点存储起来

1
2
3
4
5
6
7
8
9
10
11
// 将计时的起始时间修改为传参的形式
class AdvertisingManage(millisInFuture:Long=5000):LifecycleObserver{
...

// 计时器
private var countDownTimer:CountDownTimer?=object : CountDownTimer(millisInFuture,1000){
...
}

...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class AdvertisingActivity : BaseActivity() {
...
// 计时的时间,默认值 5 秒
var millisInFuture:Long=5000

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_advertising)

...
savedInstanceState?.let {
millisInFuture = it.getLong(KEY_MILLISINFUTURE)
}
val advertisingManage = AdvertisingManage(millisInFuture)
lifecycle.addObserver(advertisingManage)

advertisingManage.advertisingManageListener = object : AdvertisingManage.AdvertisingManageListener{
override fun timing(second: Int) {
tvAdvertisingTime.text = "广告剩余$second 秒"
millisInFuture = second.toLong() * 1000
}

...
}
...
}

override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
super.onSaveInstanceState(outState, outPersistentState)
outState.putLong(KEY_MILLISINFUTURE,millisInFuture)
}

companion object{
// key 计时的开始时间
const val KEY_MILLISINFUTURE = "keyMillsinfuture"
}
}

使用 ViewModel

1
2
3
4
5
6
7
// 如果想在 ViewModel 类中使用资源文件,则需要使用到 Context 上下文,这时不能将 Activity 的上下文传给 ViewModel,否则会存在内存泄漏的风险。
// 这种情况下需要将父类 ViewModel 修改为 AndroidViewModel 即可,拥有 Application 的实例后就可以访问资源文件了。
class AdvertisingViewModel(application: Application) : AndroidViewModel(application) {

// 计时开始的时间,默认值 5 秒
var millisInFuture:Long=5000
}
1
2
3
4
5
6
7
8
class AdvertisingManage(millisInFuture:Long=5000):LifecycleObserver{
...
// 计时器
private var countDownTimer:CountDownTimer?=object : CountDownTimer(millisInFuture,1000){
...
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class AdvertisingActivity : BaseActivity() {
private val TAG = "TAG_AdvertisingActivity"
// 跳过广告按钮
lateinit var btnIngore:Button
// 广告时间
lateinit var tvAdvertisingTime:TextView

private lateinit var advertisingViewModel: AdvertisingViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_advertising)

initView()
initData()
}

private fun initData() {
// 初始化
advertisingViewModel = ViewModelProvider(this).get(AdvertisingViewModel::class.java)

val advertisingManage = AdvertisingManage(advertisingViewModel.millisInFuture)
lifecycle.addObserver(advertisingManage)

advertisingManage.advertisingManageListener = object : AdvertisingManage.AdvertisingManageListener{
override fun timing(second: Int) {
tvAdvertisingTime.text = "广告剩余$second 秒"
advertisingViewModel.millisInFuture = second.toLong() * 1000
}

override fun enterMainActivity() {
MainActivity.actionStart(this@AdvertisingActivity)
finish()
}
}
}

private fun initView() {
btnIngore = findViewById(R.id.btn_ignore) as Button
tvAdvertisingTime = findViewById(R.id.tv_advertising_time) as TextView

// 跳过广告点击事件
btnIngore.setOnClickListener{
MainActivity.actionStart(this)
finish()
}
}
}

使用 LiveData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class AdvertisingViewModel(application: Application) : AndroidViewModel(application) {

/**
* 计时开始的时间,默认值 5 秒
*/
var millisInFuture:Long=5000

/**
* 计时结束
*
* 声明为私有类型,保证数据操作的完整性。
* 声明为私有类型变量后,则没办法在 Activity 中观察数据的变化,
* 所以一般会单独声明非私有类型 LiveData 类型的变量,并给他赋值为 timingResult
*/
private var timingResult = MutableLiveData<Long>()

val _timingResult:LiveData<Long>
get() = timingResult

fun setTimingResult(millisInFuture: Long){
// MutableLiveData 数据赋值有两种方法,分别为 postValue 和 setValue
// 区别在于,当设置数据操作在子线程时使用前者,在 UI 线程时使用后者。
timingResult.value = millisInFuture
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class AdvertisingManage(advertisingViewModel: AdvertisingViewModel):LifecycleObserver{
val TAG = "TAG_AdvertisingManage"

// 计时器
private var countDownTimer:CountDownTimer?=object : CountDownTimer(advertisingViewModel.millisInFuture,1000){
override fun onTick(millisUntilFinished: Long) {
Log.d(TAG,"广告剩余时间${(millisUntilFinished/1000).toInt()}秒")
advertisingViewModel.setTimingResult(millisUntilFinished/1000)
}

override fun onFinish() {
Log.d(TAG,"广告结束,准备进入主界面")
}
}

/**
* 开始计时
*/
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun start(){
Log.d(TAG,"开始计时")
countDownTimer?.start()
}

/**
* 停止计时
*/
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onCancel(){
Log.d(TAG,"停止计时")
countDownTimer?.cancel()
countDownTimer = null
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class AdvertisingActivity : BaseActivity() {
private val TAG = "TAG_AdvertisingActivity"
// 跳过广告按钮
lateinit var btnIngore:Button
// 广告时间
lateinit var tvAdvertisingTime:TextView

private lateinit var advertisingViewModel: AdvertisingViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_advertising)

initView()
initData()
}

private fun initData() {
advertisingViewModel = ViewModelProvider(this).get(AdvertisingViewModel::class.java)

val advertisingManage = AdvertisingManage(advertisingViewModel)
lifecycle.addObserver(advertisingManage)

// 由于 LiveData 类型的变量值是不可变的,即无法给 LiveData 类型的变量重新赋值,
// 所以在 Activity 中对 _timingResult 进行观察,
// 通过 ViewModel 的 setTimingResult 方法对 timingResult 进行赋值,这样就保证了数据的完整性。
advertisingViewModel._timingResult.observe(this, Observer {
tvAdvertisingTime.text = "广告剩余$it 秒"
if (it == 0L){
Log.d(TAG,"广告结束,准备进入主页面")
MainActivity.actionStart(this@AdvertisingActivity)
finish()
}
})

// 日志打印:
// D/>>>>>>>>TAG_BaseActivity: AdvertisingActivity
// D/TAG_AdvertisingManage: 开始计时
// D/TAG_AdvertisingManage: 广告剩余时间4秒
// D/TAG_AdvertisingManage: 广告剩余时间3秒
// D/TAG_AdvertisingManage: 广告剩余时间2秒
// D/TAG_AdvertisingManage: 广告剩余时间1秒
// D/TAG_AdvertisingManage: 广告剩余时间0秒
// D/TAG_AdvertisingActivity: 广告结束,准备进入主页面
// D/>>>>>>>>TAG_BaseActivity: MainActivity
// D/TAG_AdvertisingManage: 广告结束,准备进入主界面
// D/TAG_AdvertisingManage: 停止计时
}

private fun initView() {
btnIngore = findViewById(R.id.btn_ignore) as Button
tvAdvertisingTime = findViewById(R.id.tv_advertising_time) as TextView

// 跳过广告点击事件
btnIngore.setOnClickListener{
MainActivity.actionStart(this)
finish()
}
}
}

备注

参考资料:

《Android Jetpack开发 原理解析与应用实战》

欢迎关注微信公众号:非也缘也