Android打造丝滑的Activity recreate重建(主题切换)过渡动画

前言

当应用程序支持多种语言或主题时,切换语言或主题通常需要重新启动 Activity 以重新加载配置。虽然 recreate 是一种常用的重建 Activity 方法,但它不支持像在 Activity 之间切换时那样使用过渡动画。特别是在切换 浅色/深色 主题时,由于缺乏过渡动画而显得很生硬。为了提升改善这一点,只能自己实现过渡动画以提供更流畅的用户体验。

一开始,我考虑在保存状态时使用 onSaveInstanceState 将 activity.window.decorView 绘制成位图并保存到 outState 中。然后在 onCreate 中读取该位图,并通过 WindowManager 在整个屏幕上显示一个铺满的 ImageView,将位图显示在 ImageView 上并执行动画。然而,我尝试后发现 WindowManager 的显示会比 Activity 晚一些,导致出现了闪屏的情况。

在我继续思考的过程中,偶然发现了一篇博客:Change Theme Dynamically with Circular Reveal Animation on Android。原来我与大佬的想法只有一步之差。该博客中的方法是在 Activity 的布局中添加一个铺满全屏的 ImageView,并将其 visibility 设置为 gone。这样,我们就可以在需要时将位图显示在 ImageView 上,而不需要使用 WindowManager。恍然大悟,我怎么没想到呢!🌟

效果

废话不多说,以下是 Demo 实现的效果
请添加图片描述

Demo源码放在了最下面

步骤

大致分为以下几步:

  1. 设置Activity为全屏显示
    确保Activity占据整个屏幕空间,去除状态栏和导航栏的影响。
  2. 添加隐藏的ImageView
    在Activity原有的布局顶部添加一个占满全屏的ImageView,默认隐藏。
    用于在主题切换后显示Activity重建前保存的Bitmap
  3. 修改主题后保存状态并重建activity
    当用户切换主题时,先将当前Activity的decorView绘制为Bitmap保存到状态
    recreate重新创建Activity以更新主题
  4. activity重启后通过保存的状态执行动画
    在Activity重建后,通过之前保存的状态恢复界面内容并执行揭露动画

将Activity设置为全屏

我这里使用一个BaseActivity来作为基础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

随便写的布局,只需要关注ClipImageView就好了

<?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>

布局的显示效果
在这里插入图片描述

ClipImageView是我为了方便使用动画实现的一个继承自ImageView的自定义View,在后面执行动画时用到

/**
 * 可以裁切的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

这个Activity继承自上面实现的BaseActivity,因此无需关心设置主题和activity全屏显示的问题。
MainActivity 的 transitionRecreate 方法实现了以下步骤:

  1. 获取切换主题的 Toolbar 中的 menu 按钮中心点(后面用作圆形揭露动画的中心点)
  2. 将当前 Activity 绘制到 Bitmap
  3. 将这些数据赋值给 recreateTransitionData 属性
  4. 调用 recreate 方法开始重建 Activity

在recreate调用后,onSaveInstanceState 会被调用以保存状态,在这里将 recreateTransitionData 属性值保存到状态中

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重启后通过保存的状态执行动画

在onCreate被调用时,通过保存的状态判断是否需要执行过渡动画

transitionAnimation 方法负责为Activity创建过渡动画。该方法接受一个TransitionData类型的参数,这个参数包含了动画所需的信息。
在方法的开始,ImageView ivTransition被设置为可见,并且其位图被设置为transitionData对象中的screenBitmap(Activity重建前绘制保存的显示内容)。
此时用户看到的 Activity 将呈现出 Activity 重建前的效果,从而营造出 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()
    }
}

OK,大功告成。只需要在切换主题时调用transitionRecreate方法即可实现使用过渡动画重建activity

Demo源码

https://github.com/Thousand-Dust/DemoActivityRecreateTransition

  • 26
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: 要在Android上重启一个Activity,可以使用 Intent 和 finish() 方法。 例如: ``` Intent intent = new Intent(CurrentActivity.this, CurrentActivity.class); finish(); startActivity(intent); ``` 首先,创建一个新的 Intent 对象,并将当前 Activity 作为上下文传入。 然后调用 finish() 方法结束当前 Activity,最后调用 startActivity(intent) 方法重新启动该 Activity。 ### 回答2: 在Android中,可以通过以下几种方式来重启Activity。 1. 使用Intent重新启动Activity:可以通过创建一个新的Intent对象,并将原Activity的类名和其他需要传递的数据传递给新的Intent,然后调用startActivity()方法来启动新的Activity,并使用finish()方法来销毁原Activity。 2. 使用FLAG_ACTIVITY_CLEAR_TOP标志重新启动Activity:可以在Intent中设置FLAG_ACTIVITY_CLEAR_TOP标志,然后调用startActivity()方法来启动新的Activity。这个标志会检查Activity的任务栈,如果需要启动的Activity已经在任务栈中存在,那么会将该Activity之上的所有Activity都销毁掉,然后重用已存在的Activity,并将其置于栈顶。 3. 使用Recreate()方法重启Activity:在Android 3.0及以上版本中,可以通过调用Activity的recreate()方法来重启Activity。这个方法会销毁当前的Activity,然后重新创建一个新的Activity,并将其置于栈顶。 需要注意的是,以上这些方法都会导致Activity的生命周期方法被重新调用,如onCreate()、onStart()、onResume()等,所以在处理重启逻辑时,需要注意保存和恢复Activity的状态。 另外,可以根据具体的需求选择合适的重启方式,如是否需要传递数据、是否需要清空任务栈等。根据不同的场景,选择合适的方法可以更好地实现Activity的重启功能。 ### 回答3: 在Android中,我们可以通过调用`startActivity()`方法来启动一个新的Activity。当我们希望重新启动当前Activity时,有以下几种常见的方法。 1. 使用`Intent.FLAG_ACTIVITY_CLEAR_TOP`标志: 通过在启动新Activity时设置`Intent.FLAG_ACTIVITY_CLEAR_TOP`标志,我们可以清除当前Activity之上的所有Activity。这样,在新的Activity启动后,原来的Activity会被销毁并被新的Activity覆盖。代码如下: ```java Intent intent = new Intent(this, NewActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); finish(); ``` 2. 使用`Intent.FLAG_ACTIVITY_NEW_TASK`标志: 通过在启动新的Activity时设置`Intent.FLAG_ACTIVITY_NEW_TASK`标志,我们可以在新的任务栈中启动新的Activity。这样,原来的Activity会被销毁,并且返回到主界面,而新的Activity将会在新的任务栈中启动。代码如下: ```java Intent intent = new Intent(this, NewActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); finish(); ``` 3. 通过重新启动Activity: 通过重新调用`onCreate()`方法和`onStart()`方法,我们可以重新启动当前Activity。代码如下: ```java Intent intent = getIntent(); finish(); startActivity(intent); ``` 以上是三种常见的Android重启Activity的方法,开发者可以根据具体的需求选择合适的方法来实现重启。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值