其他
Android打造丝滑的Activity recreate重建(主题切换)过渡动画
The following article is from Android补给站 Author GNAHZ_A
Demo源码放在了最下面。
设置Activity为全屏显示确保Activity占据整个屏幕空间,去除状态栏和导航栏的影响。 添加隐藏的ImageView在Activity原有的布局顶部添加一个占满全屏的ImageView,默认隐藏。用于在主题切换后显示Activity重建前保存的Bitmap。 修改主题后保存状态并重建activity当用户切换主题时,先将当前Activity的decorView绘制为Bitmap保存到状态recreate重新创建Activity以更新主题。 activity重启后通过保存的状态执行动画在Activity重建后,通过之前保存的状态恢复界面内容并执行揭露动画。
将Activity设置为全屏
/**
* 基础 Activity
* 实现了加载本地配置的主题和语言
* @author Thousand-Dust
*/
abstract class BaseActivity : AppCompatActivity() {
override fun attachBaseContext(newBase: Context) {
// 加载本地配置的主题
val theme = AppGlobals.getTheme()
delegate.localNightMode = theme.mode
// val config = newBase.resources.configuration
// 加载本地配置的语言
// val language = AppGlobals.getLanguage()
// config.setLocale(language.locale)
// val context = newBase.createConfigurationContext(config)
// super.attachBaseContext(context)
return super.attachBaseContext(newBase)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Activity全屏显示,隐藏状态栏和导航栏
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
} else {
window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
}
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
}
}
在Activity原有的布局顶部添加一个隐藏的ImageView
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/app_name"
android:background="?attr/colorPrimary"
android:paddingTop="10dp"
app:menu="@menu/main_menu" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.355" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:text="Hello World!"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<com.td.demoactivityrecreatetransition.ClipImageView
android:id="@+id/iv_transition"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
/**
* 可以裁切的ImageView
* @author Thousand-Dust
*/
class ClipImageView : androidx.appcompat.widget.AppCompatImageView {
/**
* 裁切类型
*/
enum class ClipType {
/**
* 圆形
*/
CIRCLE,
/**
* 圆形(反向裁切)
*/
CIRCLE_REVERSE,
}
/**
* 裁切类型
*/
private var clipType = ClipType.CIRCLE
/**
* 裁切区域
*/
private var clipPath = Path()
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
/**
* 清空裁切
*/
fun clearClip() {
clipPath.reset()
invalidate()
}
/**
* 裁切圆形
* @param centerX 圆心X
* @param centerY 圆心Y
* @param radius 半径
* @param clipType 裁切类型
*/
fun clipCircle(centerX: Float, centerY: Float, radius: Float, clipType: ClipType) {
clipPath.reset()
clipPath.addCircle(centerX, centerY, radius, Path.Direction.CW)
this.clipType = clipType
invalidate()
}
override fun onDraw(canvas: Canvas) {
if (!clipPath.isEmpty) {
canvas.save()
when (clipType) {
ClipType.CIRCLE -> {
// 裁切圆形
canvas.clipPath(clipPath)
}
ClipType.CIRCLE_REVERSE -> {
// 反向裁切圆形
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(clipPath)
} else {
canvas.clipPath(clipPath, Region.Op.DIFFERENCE)
}
}
}
}
// 绘制图片
super.onDraw(canvas)
if (!clipPath.isEmpty) {
canvas.restore()
}
}
}
修改主题后保存状态并重建activity
获取切换主题的 Toolbar 中的 menu 按钮中心点(后面用作圆形揭露动画的中心点)。 将当前 Activity 绘制到 Bitmap。 将这些数据赋值给 recreateTransitionData 属性。 调用 recreate 方法开始重建 Activity。
class MainActivity : BaseActivity() {
private lateinit var toolbar: Toolbar
private var recreateTransitionData: TransitionData? = null
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
...
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (recreateTransitionData != null) {
// 保存重建过渡动画 data 到状态
outState.putParcelable(TRANSITION_DATA_KEY, recreateTransitionData)
}
}
/**
* 使用过渡动画重建(recreate)Activity
*/
private fun transitionRecreate(type: TransitionType) {
// 获取切换主题menu的坐标(以menu的中心点为圆形揭露动画的中心点)
val menuItemView = toolbar.menu.findItem(R.id.menu_theme_toggle).let {
toolbar.findViewById<View>(it.itemId)
}
val location = IntArray(2)
menuItemView.getLocationOnScreen(location)
val centerX = location[0] + menuItemView.width / 2f
val centerY = location[1] + menuItemView.height / 2f
// Activity截图
val screenBitmap = window.decorView.drawToBitmap()
recreateTransitionData = TransitionData(centerX, centerY, screenBitmap, type)
// 重建Activity
recreate()
}
private fun initView() {
toolbar = findViewById(R.id.toolbar)
...
}
}
// -------- AppGlobals.kt --------
object AppGlobals {
const val THEME_KEY = "theme"
lateinit var appContext: Context
private set
private lateinit var appConfigSP: SharedPreferences
/**
* Application创建时调用初始化
*/
fun init(appContext: Context) {
this.appContext = appContext
appConfigSP = this.appContext.getSharedPreferences("AppConfig", Context.MODE_PRIVATE)
}
/**
* 获取主题配置
*/
fun getTheme(): AppTheme {
val name = appConfigSP.getString(THEME_KEY, AppTheme.AUTO.name)!!
return AppTheme.valueOf(name)
}
/**
* 写入主题配置
*/
fun setTheme(theme: AppTheme) {
if (theme == AppTheme.AUTO) {
// delete theme
appConfigSP.edit().remove(THEME_KEY).apply()
return
}
appConfigSP.edit().putString(THEME_KEY, theme.name).apply()
}
}
/**
* 支持的主题
*/
enum class AppTheme(val mode: Int) {
AUTO(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM),
LIGHT(AppCompatDelegate.MODE_NIGHT_NO),
DARK(AppCompatDelegate.MODE_NIGHT_YES);
companion object {
fun byMode(mode: Int): AppTheme {
return values().firstOrNull { it.mode == mode } ?: AUTO
}
}
}
// -------- RecreateTransition.kt --------
enum class TransitionType {
/**
* 进入
*/
ENTER,
/**
* 退出
*/
EXIT
}
/**
* 重建过渡动画 data
* 实现Parcelable接口,用于Activity重建时保存和恢复数据
*/
class TransitionData(
val centerX: Float,
val centerY: Float,
val screenBitmap: Bitmap,
val type: TransitionType,
) : Parcelable {
constructor(parcel: android.os.Parcel) : this(
parcel.readFloat(),
parcel.readFloat(),
parcel.readParcelable(Bitmap::class.java.classLoader)!!,
TransitionType.valueOf(parcel.readString()!!)
)
override fun writeToParcel(parcel: android.os.Parcel, flags: Int) {
parcel.writeFloat(centerX)
parcel.writeFloat(centerY)
parcel.writeParcelable(screenBitmap, flags)
parcel.writeString(type.name)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<TransitionData> {
override fun createFromParcel(parcel: android.os.Parcel): TransitionData {
return TransitionData(parcel)
}
override fun newArray(size: Int): Array<TransitionData?> {
return arrayOfNulls(size)
}
}
}
activity重启后通过保存的状态执行动画
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
// 重建过渡动画
if (savedInstanceState != null)
savedInstanceState.getParcelable<TransitionData>(TRANSITION_DATA_KEY)?.let {
transitionAnimation(it)
}
}
/**
* 过渡动画
*/
private fun transitionAnimation(transitionData: TransitionData) {
// 使用隐藏的 ImageView 显示bitmap
ivTransition.visibility = View.VISIBLE
ivTransition.setImageBitmap(transitionData.screenBitmap)
ivTransition.post {
val animator = ValueAnimator.ofFloat()
var clipType = ClipImageView.ClipType.CIRCLE
when (transitionData.type) {
TransitionType.ENTER -> {
// 进入动画,裁切掉圆内的区域 圆由小变大
animator.setFloatValues(
0f,
hypot(ivTransition.width.toFloat(), ivTransition.height.toFloat())
)
clipType = ClipImageView.ClipType.CIRCLE_REVERSE
}
TransitionType.EXIT -> {
// 退出动画,裁切掉圆外的区域 圆由大变小
animator.setFloatValues(
hypot(
ivTransition.width.toFloat(),
ivTransition.height.toFloat()
),
0f
)
clipType = ClipImageView.ClipType.CIRCLE
}
}
animator.duration =
resources.getInteger(android.R.integer.config_longAnimTime).toLong()
animator.addListener(
onEnd = {
// 动画结束后隐藏 ImageView
ivTransition.visibility = View.GONE
}
)
animator.addUpdateListener {
val radius = it.animatedValue as Float
// 更新裁切区域
ivTransition.clipCircle(
transitionData.centerX,
transitionData.centerY,
radius,
clipType
)
}
animator.start()
}
}
Demo源码
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
Android hide api反射方案合集
OpenHarmony源码系列:如何触发UI刷新?
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!