Jetpack新成员SplashScreen:为全新的应用启动效果赋能!(2)

简介: Jetpack新成员SplashScreen:为全新的应用启动效果赋能!(2)

4. 定制退场效果

为了使得启动画面能完美过渡到目标画面,退场效果的定制格外重要。而且相较于进场只能定制Icon动画,退场则能定制整体和Icon的各式动画,空间更大,也更灵活。

4.1 执行退场动画

Splash Screen Window在退出的时候,是执行退场动画的时机。通过SplashScreen库提供的OnExitAnimationListener API可以拿到这个时机,它同时会返回启动画面视图,便于后续的定制。


不同于Android 12,SplashScreen库启动画面视图统一封装到了SplashScreenViewProvider中。这个API将依据版本返回对应视图:低版本是自定义的FrameLayout,12则是版本专属的SplashScreenView。

fun customizeSplashScreen() {
    ...
    customizeSplashScreenExit()
}
// Customize splash screen exit animator.
private fun customizeSplashScreenExit() {
    splashScreen.setOnExitAnimationListener { splashScreenViewProvider ->
        val onExit = {
            splashScreenViewProvider.remove()
        }
        showSplashExitAnimator(splashScreenViewProvider.view, onExit)
        showSplashIconExitAnimator(splashScreenViewProvider.iconView, onExit)
    }
}

无论运行在哪个版本,通过统一提供的SplashScreenViewProvider实例,可以执行一致的动画效果。

比如针对整体视图做缩放和淡出动画。

private fun showSplashExitAnimator(splashScreenView: View, onExit: () -> Unit = {}) {
    val alphaOut = ObjectAnimator.ofFloat(
        splashScreenView,
        ...
    )
    val scaleOut = ObjectAnimator.ofFloat( ... )
    AnimatorSet().run {
        duration = defaultExitDuration
        interpolator = AnticipateInterpolator()
        playTogether(scaleOut, alphaOut)
        start()
    }
}

针对Icon视图做缩放、淡出和位移的动画。目的是让小鸟渐渐缩小并上移至游戏画面中,即无缝过渡到游戏区域。

private fun showSplashIconExitAnimator(iconView: View, onExit: () -> Unit = {}) {
    val alphaOut = ObjectAnimator.ofFloat(
        iconView,
        ...
    )
    val scaleOut = ObjectAnimator.ofFloat( ... )
    val slideUp = ObjectAnimator.ofFloat( ... )
    AnimatorSet().run {
        ...
        playTogether(alphaOut, scaleOut, slideUp)
        doOnEnd {
            onExit()
        }
        start()
    }
}


注意:退场动画结束后一定要调用remove()手动移除视图,否则可能会看到启动画面一直盖在App上,无法消失。对于12来说,这实际上是系统返回的SplashScreenView Surface一直残留在画面上,而低版本则是FrameLayout仍残存在ContentView树里。

4.2 优化退场动画时长

App开始描画的时候,无论进场动画是否执行完,Splash Screen Window都将退出,同时执行预设的退场动画。 而设备性能的优劣或负荷状态的不同,又会影响App描画的开始时机,所以退场动画的执行时机并不固定,随着设备的状况略微变化。


如果App描画得早,进场动画可能还没执行完。为了让用户更快看到目标内容,退场动画可以缩短执行时长,比如直接沿用进场动画的剩余时长


相反,如果描画得晚,进场动画早结束了。如果退场动画还再占用时间,那将严重耽误用户看到目标内容,造成启动卡顿的坏印象。所以这时候,退场动画可以执行很短的固定时长,甚至可以不执行

1832b220aa754cd18c504acc7686a560.png

换句话说,退场动画本意是后台加载的缓冲和过渡,不能为了单纯展示动画牺牲启动体验,可以灵活控制退场的占用时长。

为了便于计算进场Icon动画的剩余时长,SplashScreen库提供了获取其开始时刻和总时长的API:

/**
 * Start time of the icon animation.
 *
 * On API 31+, returns the number of millisecond since the Epoch time (1970-1-1T00:00:00Z)
 *
 * Below API 31, returns 0 because the icon cannot be animated.
 */
public val iconAnimationStartMillis: Long get() = impl.iconAnimationStartMillis
/**
 * Duration of the icon animation as provided in attr.
 */
public val iconAnimationDurationMillis: Long get() = impl.iconAnimationDurationMillis

下面的代码演示了在退场动画执行前判断下进场动画是否完毕,完毕的话沿用动画剩余的时间,否则放弃执行。

private fun getRemainingDuration(splashScreenView: SplashScreenViewProvider): Long {
    // Get the duration of the animated vector drawable.
    val animationDuration = splashScreenView.iconAnimationDurationMillis
    // Get the start time of the animation.
    val animationStart = splashScreenView.iconAnimationStartMillis
    // Calculate the remaining duration of the animation.
    return if (animationDuration == 0L || animationStart == 0L)
        defaultExitDuration
    else (animationDuration - SystemClock.uptimeMillis() + animationStart)
        .coerceAtLeast(0L)
}

前面说过低版本在进场的时候不支持Icon动画,那自然没有必要计算剩余时长。所以运行在低版本上的话,这两个API总是返回默认值0,值得留意!

private open class ViewImpl(val activity: Activity) {
    open val iconAnimationStartMillis: Long get() = 0
    open val iconAnimationDurationMillis: Long get() = 0
    ...
}

5. SplashScreen实现原理

Android 12的源码尚未公开,下面针对Jetpack SplashScreen库的源码进行解读。※之前觉得SplashScreen库提供的API简单明了,原理应该也不复杂。但深究源码后发现,不少细节和猜测存在出入,还是值得反复思考的。

5.1 总体架构

Activity通过SplashScreen库提供的SplashScreen实例可以获取到控制启动画面的入口,其内部将依据OS版本决定采用12的专属API还是自定义视图,来展示、延时或移除启动画面。

1832b220aa754cd18c504acc7686a560.png

5.2 installSplashScreen

获取SplashScreen实例的installSplashScreen()将读取并设置目标Activity的Theme。如果运行在低版本上的话,还需要获取Icon和Background的配置。此刻只是获取,并未将退场用的自定义视图添加上来。

1832b220aa754cd18c504acc7686a560.png

5.3 setKeepVisibleCondition

通过setKeepVisibleCondition()可以延长启动画面的展示,无关运行的版本,原理都是向ContentView的ViewTreeObserver注册OnPreDrawListener回调来实现。描画放行的时候,低版本额外需要手动执行退出的回调,12则由系统自行执行。


这个时候退场用的自定义视图仍然还没添加上来,只是延迟了Splash Screen Window的退出而已。

1832b220aa754cd18c504acc7686a560.png

5.4 setOnExitAnimationListener

setOnExitAnimationListener()可以监听退场时机,SplashScreen会将启动画面视图准备好封装到SplashScreenViewProvider中,然后在启动画面需要退出的时候,通过OnExitAnimationListener接口回调。

1832b220aa754cd18c504acc7686a560.png

低版本上视图相关的处理比较多,需要inflate一个自定义布局并添加到ContentView中,然后将install准备好的background和icon反映进来。

面向低版本添加的自定义布局 :

<FrameLayout ...>
  <ImageView
      android:id="@+id/splashscreen_icon_view"
      android:layout_width="@dimen/splashscreen_icon_size"
      android:layout_height="@dimen/splashscreen_icon_size"
      android:layout_gravity="center" />
</FrameLayout>

5.5 adjustInsets特殊处理

SplashScreenViewProvider初始化后会额外调用adjustInsets(),而且只有面向低版本才实现了具体逻辑。一起来研究下这个特殊处理的用意。


先来看下函数的注释:


Adjust the insets to avoid any jump between the actual splash screen and the SplashScreen View.


字面意思是为了避免启动画面和SplashView视图之间发生跳跃,需要调整下视图的参数。好像还不是很理解,再结合下函数的具体实现:

private class Impl23(activity: Activity) : Impl(activity) {
    override fun adjustInsets(
        view: View,
        splashScreenViewProvider: SplashScreenViewProvider
    ) {
        // Offset the icon if the insets have changed
        val rootWindowInsets = view.rootWindowInsets
        val ty =
            rootWindowInsets.systemWindowInsetTop - rootWindowInsets.systemWindowInsetBottom
        splashScreenViewProvider.iconView.translationY = -ty.toFloat() / 2f
    }
}

默认的adjustInsets()没有具体实现,只有低版本才有具体实现,这又是为什么?


处理的内容很直白:监听SplashScreenViewProvider里存放的View的布局变化,从中获取rootWindowInsets,将覆盖在Window的状态栏和导航栏的高度差值取中值,然后将Icon视图向上或向下进行移动。


前面的原理提到过低版本的进场效果实际上是将一个Layer Drawable设置到了Window Background上,Icon在该Drawable里的位置是居中的,所以Icon在Splash Screen Window里也完全居中

但退场效果的画面视图并非Window Background Drawable,而是向ContentView中手动添加的Framelayout布局。对于布局来说,Icon视图是居中的,但对于它所属的Window来说,因顶部的Statusbar和底部的NavigationBar的高度不同,最终Icon视图在Window整体的位置并不完全居中。尤其当设备采用Gesture Navigation导航模式的话,Icon视图偏下的情况更明显

12的退场视图可以说是Splash Screen Window的完整拷贝,不是自定义的ContentView子布局,不存在这个问题

总结来说,如果不加干预的话,低版本上从进场的Window Background到退场的FrameLayout时,App的Icon会发生错位或跳跃的违和感!

默认的adjustInsets()没有具体实现,只有低版本才有具体实现,这又是为什么?


处理的内容很直白:监听SplashScreenViewProvider里存放的View的布局变化,从中获取rootWindowInsets,将覆盖在Window的状态栏和导航栏的高度差值取中值,然后将Icon视图向上或向下进行移动。


前面的原理提到过低版本的进场效果实际上是将一个Layer Drawable设置到了Window Background上,Icon在该Drawable里的位置是居中的,所以Icon在Splash Screen Window里也完全居中

但退场效果的画面视图并非Window Background Drawable,而是向ContentView中手动添加的Framelayout布局。对于布局来说,Icon视图是居中的,但对于它所属的Window来说,因顶部的Statusbar和底部的NavigationBar的高度不同,最终Icon视图在Window整体的位置并不完全居中。尤其当设备采用Gesture Navigation导航模式的话,Icon视图偏下的情况更明显

12的退场视图可以说是Splash Screen Window的完整拷贝,不是自定义的ContentView子布局,不存在这个问题

总结来说,如果不加干预的话,低版本上从进场的Window Background到退场的FrameLayout时,App的Icon会发生错位或跳跃的违和感!

5.6 remove

setOnExitAnimationListener调用后,SplashScreen库会将启动画面的视图回调回来,执行完退场动画后需要调用remove()手动移除视图,它的原理很简单:


12之前就是将FrameLayout从ContentView树上移除

12则是调用专用的API即SplashScreenView#remove()

6. API总结

对SplashScreen库几个关键API做个整理:

1672154042800.png

并和Android 12提供的版本做个简单对比:

1672154057401.png

1672154070648.png

7. 本文Demo

开源地址:https://github.com/ellisonchan/ComposeBird

适配Splash Screen功能后的Compose版Flappy Bird游戏的整体效果:

8. 未决事项

通过上面的阐述已经知道:低版本上展示退场效果的启动画面,实质上是临时添加到App内部的FrameLayout。而12上显然不是这种方式,通过Dump命令发现退场的时候也没有创建额外的Window。


那么12上启动画面Window退出后提供给App定制退场效果的SplashScreenView到底是什么?

假使这个“View”*并不属于App本身,那么App能够访问它并对属性做出改动,是如何实现的?

在决定搁置这个疑惑的时候,偶然发现下12上启动画面执行过程中会打印Splash相关关键字的系统日志:


StartingSurfaceDrawer: addSplashScreen com.ellison.flappybird: nonLocalizedLabel=null theme=7f1000e7 task= 334

StartingSurfaceDrawer: window attributes color: ffecfcdd icon android.graphics.drawable.AnimatedVectorDrawable…

StartingSurfaceDrawer: fillViewWithIcon surfaceWindowView android.window.SplashScreenView…

StartingSurfaceDrawer: Copying splash screen window view for task: 334 parcelable? android.window.SplashScreenView$SplashScreenViewParcelable

StartingSurfaceDrawer: Task start finish, remove starting surface for task 334

StartingSurfaceDrawer: Removing splash screen window for task: 334


于是我大胆猜测,StartingSurfaceDrawer负责通过WMS读取、展示和移除App设置的Splash Screen Window视图。并在Window退出前通过WMS通知目标Activity的PhoneWindow,从Splash Screen Window里复原视图,然后将复原得到的SplashScreenView添加到App画面上。因为PhoneWindow直接持有DecorView,所以可能直接附着到DecorView上去。


StartingSurfaceDrawer到底是谁、猜测是否正确以及具体的细节,还有待12的源码公开后再做深入地研究~

结语

起初推测因为Android 6以上的版本已是市场主流,才决定让SplashScreen库最早兼容到这个版本。后面探究原理的时候,发现其内部调用的adjustInsets()处理依赖一个版本6才加入的API,所以又感觉这才是兼容到6的真正原因。


兼容到Android 6版本是出于占用率的考虑还是源于API的限制已不再重要,关键是Splash Screen功能已支持足够多的Android设备。


之前你们说Android 12全新的Splash Screen功能虽然很酷,但不兼容低版本,略显鸡肋。如今有了Jetpack SplashScreen库的加持,总该适配起来了吧?

参考资料

SplashScreen文档

A peek inside Jetpack Core Splashscreen

Meet the Jetpack Splashscreen API: a definitive guide for splash screens in Android

推荐阅读

Android 12上全新的应用启动画面,还不适配一下?

一气呵成:用Compose完美复刻Flappy Bird!

从Preference组件的更迭看Jetpack的前世今生

深度解读Jetpack框架的基石-AppCompat


相关文章
|
7月前
|
存储 安全 Android开发
构建高效的Android应用:Kotlin与Jetpack的结合
【5月更文挑战第31天】 在移动开发的世界中,Android 平台因其开放性和广泛的用户基础而备受开发者青睐。随着技术的进步和用户需求的不断升级,开发一个高效、流畅且易于维护的 Android 应用变得愈发重要。本文将探讨如何通过结合现代编程语言 Kotlin 和 Android Jetpack 组件来提升 Android 应用的性能和可维护性。我们将深入分析 Kotlin 语言的优势,探索 Jetpack 组件的核心功能,并通过实例演示如何在实际项目中应用这些技术。
|
7月前
|
Java 数据库 Android开发
构建高效Android应用:Kotlin与Jetpack的完美结合
【5月更文挑战第28天】 在现代移动开发领域,Android平台以其广泛的用户基础和开放性受到开发者青睐。随着技术的不断进步,Kotlin语言以其简洁性和功能性成为Android开发的首选。而Android Jetpack组件则为开发者提供了一套高质量的设计架构、工具和UI组件,以简化应用程序的开发过程。本文将探讨如何利用Kotlin语言和Android Jetpack组件共同构建一个高效的Android应用程序,涵盖从语言特性到架构模式的全面分析,并提供具体的实践指导。
|
7月前
|
XML Java Android开发
安卓开发新趋势:Jetpack Compose的兴起与应用
【5月更文挑战第25天】随着移动开发技术的不断演进,安卓平台的创新也在持续推进。近年来,一个名为Jetpack Compose的新工具集引起了开发者社区的广泛关注。本文将深入探讨Jetpack Compose的核心概念、优势以及它对现有安卓开发模式的影响,并分析其在实际项目中的应用潜力。
|
7月前
|
安全 数据库 Android开发
构建高效Android应用:采用Kotlin与Jetpack的实践指南
【5月更文挑战第22天】 在移动开发领域,Android系统因其开放性和广泛的用户基础而备受开发者青睐。随着技术的不断演进,Kotlin语言以其简洁性和功能性成为Android开发的首选语言。本文将深入探讨如何结合Kotlin和Android Jetpack组件来构建一个高效且易于维护的Android应用。我们将重点讨论如何使用Jetpack的核心组件,如LiveData、ViewModel和Room,以及Kotlin的语言特性来优化代码结构,提高应用性能,并简化数据管理。通过具体案例分析,本文旨在为开发者提供一套实用的技术指导,帮助他们在竞争激烈的市场中脱颖而出。
|
7月前
|
物联网 区块链 Android开发
构建高效Android应用:Kotlin与Jetpack的实践之路未来技术的融合潮流:区块链、物联网与虚拟现实的交汇点
【5月更文挑战第30天】 在移动开发领域,效率和性能始终是开发者追求的核心。随着技术的不断进步,Kotlin语言以其简洁性和现代化特性成为Android开发的新宠。与此同时,Jetpack组件为应用开发提供了一套经过实践检验的库、工具和指南,旨在简化复杂任务并帮助提高应用质量。本文将深入探索如何通过Kotlin结合Jetpack组件来构建一个既高效又稳定的Android应用,并分享在此过程中的最佳实践和常见陷阱。
|
7月前
|
安全 Java Android开发
构建高效Android应用:Kotlin与Jetpack实践指南
【5月更文挑战第29天】 在移动开发的世界中,效率和性能始终是核心诉求。随着技术的演进,Kotlin语言以其简洁性和功能性成为Android开发的首选。结合Jetpack组件的推广,开发者得以构建更高效、可维护且易于测试的应用。本文将深入探讨利用Kotlin语言特性以及Jetpack架构组件来优化Android应用的策略和技巧,旨在帮助开发者提升应用质量并降低维护成本。
|
7月前
|
持续交付 Android开发 开发者
构建高性能微服务架构:后端开发的终极指南构建高效Android应用:Kotlin与Jetpack的完美结合
【5月更文挑战第28天】 在现代软件开发的浪潮中,微服务架构已经成为了设计灵活、可扩展且易于维护系统的重要模式。本文将深入探讨如何构建一个高性能的微服务架构,涵盖从基础概念理解到实践策略部署的全过程。我们将讨论关键的设计原则、技术选型、性能优化技巧以及安全性考虑,旨在为后端开发者提供一个全面的指南,帮助他们构建出能够适应快速变化的市场需求和技术挑战的系统。 【5月更文挑战第28天】 在移动开发的世界中,效率和性能是衡量一个应用成功与否的关键因素。本文将深入探讨如何通过结合Kotlin语言和Android Jetpack组件,来构建一个既高效又易维护的Android应用。我们将透过实际案例分析
|
7月前
|
安全 测试技术 Android开发
构建高效Android应用:Kotlin与Jetpack的实践指南
【5月更文挑战第27天】 在移动开发的世界中,效率和性能是衡量一个应用成功与否的关键因素。对于Android开发者来说,Kotlin语言和Jetpack组件套件的出现,不仅优化了开发流程,还提升了应用的性能和稳定性。本文将深入探讨如何通过Kotlin语言结合Jetpack组件,构建出既高效又稳定的Android应用。我们将从语言特性、架构组件到实际开发案例,一步步解析Kotlin和Jetpack的协同作用,帮助开发者打造出更优质的应用体验。
|
7月前
|
安全 Java Android开发
构建高效Android应用:Kotlin与Jetpack的实践指南
【5月更文挑战第20天】 在移动开发的世界中,效率和性能始终是开发者追求的核心目标。随着技术的不断进步,Kotlin语言以其简洁、安全和实用的特性成为了Android开发的首选语言。与此同时,Android Jetpack组件的推出,为开发者提供了一套高质量的库、工具和指南,以简化应用程序的开发过程。本文将探讨如何结合Kotlin语言和Jetpack组件来构建一个高效的Android应用,涵盖从项目初始化到性能优化的全过程。
|
7月前
|
Android开发
Android Jetpack架构开发组件化应用实战,字节跳动+阿里+华为+腾讯等大厂Android面试题
Android Jetpack架构开发组件化应用实战,字节跳动+阿里+华为+腾讯等大厂Android面试题