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

这篇具有很好参考价值的文章主要介绍了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 实现的效果
Android打造丝滑的Activity recreate重建(主题切换)过渡动画,android,笔记,android,kotlin,动画

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>

布局的显示效果
Android打造丝滑的Activity recreate重建(主题切换)过渡动画,android,笔记,android,kotlin,动画

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文章来源地址https://www.toymoban.com/news/detail-835499.html

到了这里,关于Android打造丝滑的Activity recreate重建(主题切换)过渡动画的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 如何自己实现一个丝滑的流程图绘制工具(一)vue如何使用

    背景 项目需求突然叫我实现一个类似processOn一样的在线流程图绘制工具。 这可难倒我了,立马去做调研,在github上找了很多个开源的流程图绘制工具, 对比下来我还是选择了 bpmn-js 原因: 1、他的流程图是涉及到业务的,比如开始事件、结束事件等 2、扩展性很强(这个扩展

    2024年02月11日
    浏览(54)
  • 如何自己实现一个丝滑的流程图绘制工具(五)bpmn的xml和json互转

    背景 因为服务端给的数据并不是xml,而且服务端要拿的数据是json,所以我们只能xml和json互转,来完成和服务端的对接 xml转json jsonxml.js json 转为xml

    2024年02月11日
    浏览(45)
  • 如何自己实现一个丝滑的流程图绘制工具(七)bpmn-js 批量删除、复制节点

    背景 希望实现批量删除和复制节点,因为bpmn-js是canvas画的,所以不能像平时页面上的复制一样直接选择范围,会变成移动画布。 思路是: 绘制一个选择的效果框,这样才可以看出来选的节点有哪些。 上面的选中范围框效果也是用canvas画出来的 因为bpmn-js对鼠标直接选取范围

    2024年02月10日
    浏览(51)
  • Wokiee多用途Shopify主题下载,打造卓越电子商务网站

    在如今竞争激烈的电子商务市场,拥有一个卓越的网站设计是吸引用户和增加销售的关键。Wokiee多用途Shopify主题将成为您实现这一目标的利器。它提供了丰富的功能和灵活的设计选项,帮助您打造一个令人印象深刻的电子商务网站。立即下载Wokiee主题,将您的在线业务推向新

    2024年02月09日
    浏览(42)
  • Activity之间数据回传【Android、activity回传、结合实例】

    在Android应用中,有时需要从一个Activity向另一个Activity传递数据,并在第二个Activity处理后将结果传递回第一个Activity。 这种情况下,我们可以使用 startActivityForResult() 和 onActivityResult() 方法来实现数据回传。 创建新的Android项目: 打开Android Studio并创建一个新的Android项目,确

    2024年02月08日
    浏览(47)
  • Android Activity启动流程一:从Intent到Activity创建

    关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。 学习前,建议有相关知识储备: 【Android 基础】 应用(Application)启动流程 通过本文你可以学习到Activity启动流

    2024年02月10日
    浏览(47)
  • Android Activity的启动流程(Android-10)

    在Android开发中,我们经常会用到startActivity(Intent)方法,但是你知道startActivity(Intent)后Activity的启动流程吗?今天就专门讲一下最基础的startActivity(Intent)看一下Activity的启动流程,同时由于Launcher的启动后续和这里基本类似,就记录在一起。注意本章都是基于Android-10来讲解的。

    2024年01月17日
    浏览(47)
  • [Android 四大组件] --- Activity

    ​​Activity​​是一个Android的应用组件,它提供屏幕进行交互。每个Activity都会获得一个用于绘制其用户界面的窗口,窗口可以充满哦屏幕也可以小于屏幕并浮动在其他窗口之上。 一个应用通常是由多个彼此松散联系的Activity组成,一般会指定应用中的某个Activity为主活动,也

    2024年02月10日
    浏览(45)
  • Android --- Activity

    官方文档-activity Activity 提供窗口,供应在其中多个界面。此窗口通常会填满屏幕,但也可能小于屏幕并浮动在其他窗口之上。 大多数应用包含多个屏幕,这意味着它们包含多个 Activity。通常,应用中的一个 Activity 会被指定主 Activity,即用户启动应用时显示的第一个屏幕。然

    2024年04月23日
    浏览(73)
  • Android Activity启动过程详解

    1,《android系统启动流程简介》 2,《android init进程启动流程》 3,《android zygote进程启动流程》 4,《Android SystemServer进程启动流程》 5,《android launcher启动流程》 6,《Android Activity启动过程详解》 1,《Android 源码下载和编译》 2,《android 11源码编译和pixel3 刷机》 3,《Andro

    2024年02月09日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包