From 354be692c9c3d3b4dd34d76f15f0fccb08904cd5 Mon Sep 17 00:00:00 2001 From: Yenaly <2021214976@stu.cqupt.edu.cn> Date: Sun, 12 May 2024 22:23:21 +0800 Subject: [PATCH] fix: some devices cannot immediately attach view, causing animation to fail fix: animation does not display on API less than 28 --- README.md | 13 +- circularrevealswitch/build.gradle.kts | 2 +- .../CRActivityLifecycleCallback.kt | 49 +++++ .../CircularRevealSwitch.kt | 177 +++++++++++------- .../impl/DayNightModeCRSwitch.kt | 6 +- .../impl/ThemeCRSwitch.kt | 7 +- gradle/libs.versions.toml | 4 +- 7 files changed, 180 insertions(+), 78 deletions(-) create mode 100644 circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CRActivityLifecycleCallback.kt diff --git a/README.md b/README.md index f41c1c2..d2ade95 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ dependencyResolutionManagement { ```kotlin dependencies { - implementation("com.github.YenalyLiew:CircularRevealSwitch:0.2") + implementation("com.github.YenalyLiew:CircularRevealSwitch:0.3") } ``` @@ -171,6 +171,13 @@ builder.setSwitcher(); - 本库强依赖 `ActivityCompat#recreate` 方法,如果你的 Activity 在重建后 View 改变,可能会影响观感 - **不要**在 `onAnimStart` 和 `onAnimEnd` 相关回调中调用与当前 Activity 相关的组件,因为回调到这里时,旧 Activity 已经销毁,你对组件的任何操作基本都会失效,但你可以在这里执行一些 application 级操作。如果你需要在旧 Activity 未销毁之前执行其他任务,可以在 `onClick` 中执行 - 建议至少是 ComponentActivity -- 建议 API 大于等于 28(Android 9.0),因为操作依赖于 `Handler#post`,而 `ActivityCompat#recreate` 中 API 低于 28 会 Handler 套 Handler 再 recreate,放入主消息队列的顺序可能会不一样,动画可能失效,但其实一般不会失效 +- ~~建议 API 大于等于 28(Android 9.0),因为操作依赖于 `Handler#post`,而 `ActivityCompat#recreate` 中 API 低于 28 会 Handler 套 Handler 再 recreate,放入主消息队列的顺序可能会不一样,动画可能失效,但其实一般不会失效~~(`v0.3` 已解决) - `setSwitcher()` 方法会覆盖原本的 `onTouch()` 与 `onClick()` 方法,如果需要点击事件,可以从构造器里调用,或者继承 CircularRevealSwitch 类重写相应的方法 -- 本库只提供切换主题效果,不提供切换后主题的保存。如果你需要保存主题,希望下次启动软件或者打开新 Activity 时能应用到新主题,请自行储存并设置 \ No newline at end of file +- 本库只提供切换主题效果,不提供切换后主题的保存。如果你需要保存主题,希望下次启动软件或者打开新 Activity 时能应用到新主题,请自行储存并设置 + +## 更新 + +### v0.3 + +1. 修复 #1:部分手机不能立即 attach view 从而导致无法启动动画的问题 +2. API 小于 28 时动画不能显示的问题 \ No newline at end of file diff --git a/circularrevealswitch/build.gradle.kts b/circularrevealswitch/build.gradle.kts index 55669f8..0118b44 100644 --- a/circularrevealswitch/build.gradle.kts +++ b/circularrevealswitch/build.gradle.kts @@ -42,7 +42,7 @@ android { afterEvaluate { publishing { - val versionName = "0.2" + val versionName = "0.3" publications { create("release") { from(components["release"]) diff --git a/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CRActivityLifecycleCallback.kt b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CRActivityLifecycleCallback.kt new file mode 100644 index 0000000..4877827 --- /dev/null +++ b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CRActivityLifecycleCallback.kt @@ -0,0 +1,49 @@ +package com.yenaly.circularrevealswitch + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import android.util.Log +import java.lang.ref.WeakReference + +/** + * This is an implementation of the ActivityLifecycleCallbacks interface. + * It is used to track the lifecycle of the current activity. + * The current activity is stored as a weak reference to prevent memory leaks. + * This class is particularly useful in scenarios where the DecorView changes after the activity is recreated, + * as it allows us to get the new activity and obtain the new DecorView. + * + * In some devices, the DecorView changes after Activity is recreated, + * which prevents the smooth reuse of DecorView. + * To ensure compatibility, we use ActivityLifecycleCallback to get the new activity + * and obtain the new DecorView. + */ +object CRActivityLifecycleCallback : Application.ActivityLifecycleCallbacks { + + var currentActivity: WeakReference? = null + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + activity.application.unregisterActivityLifecycleCallbacks(this) + if (BuildConfig.ENABLE_LOGGING) { + Log.d("CRActivityLifecycleCallback", "onActivityCreated: $activity") + } + currentActivity = activity.weak() + } + + override fun onActivityStarted(activity: Activity) = Unit + + override fun onActivityResumed(activity: Activity) = Unit + + override fun onActivityPaused(activity: Activity) = Unit + + override fun onActivityStopped(activity: Activity) = Unit + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit + + override fun onActivityDestroyed(activity: Activity) { + if (BuildConfig.ENABLE_LOGGING) { + Log.d("CRActivityLifecycleCallback", "onActivityDestroyed: $activity") + } + currentActivity?.clear() + } +} \ No newline at end of file diff --git a/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CircularRevealSwitch.kt b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CircularRevealSwitch.kt index a199bef..b105bed 100644 --- a/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CircularRevealSwitch.kt +++ b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CircularRevealSwitch.kt @@ -3,14 +3,15 @@ package com.yenaly.circularrevealswitch import android.animation.Animator import android.annotation.SuppressLint import android.app.Activity +import android.app.Application import android.content.Context import android.graphics.Bitmap +import android.os.Build import android.os.Handler import android.os.Looper import android.util.Log import android.view.MotionEvent import android.view.View -import android.view.View.OnClickListener import android.view.ViewAnimationUtils import android.view.ViewGroup import android.view.Window @@ -18,6 +19,7 @@ import android.view.animation.Interpolator import android.widget.ImageView import androidx.core.animation.addListener import androidx.core.view.children +import androidx.core.view.doOnAttach import androidx.core.view.drawToBitmap import androidx.core.view.isInvisible import androidx.lifecycle.Lifecycle @@ -35,7 +37,8 @@ import kotlin.math.hypot * @param crSwitchBuilder The builder object that contains the configuration for the circular reveal switch. */ abstract class CircularRevealSwitch>(crSwitchBuilder: T) : - View.OnTouchListener, OnClickListener, LifecycleEventObserver { + View.OnTouchListener, View.OnClickListener, + LifecycleEventObserver { companion object { const val TAG = "CircularRevealSwitch" @@ -67,21 +70,24 @@ abstract class CircularRevealSwitch>(crSwitchBuilder: T) @JvmField protected val applicationContext: Context = context.get()!!.applicationContext + @JvmField + protected val application: Application = applicationContext as Application + // The activity of the context @JvmField - protected val activity: WeakReference = context.get()!!.activity.weak() + protected var activity: WeakReference = context.get()!!.activity.weak() // The window of the activity @JvmField - protected val window: WeakReference = activity.get()!!.window.weak() + protected var window: Window = activity.get()!!.window // The decor view of the window @JvmField - protected val decorView: ViewGroup = window.get()!!.decorView as ViewGroup + protected var decorView: ViewGroup = window.decorView as ViewGroup // OnClickListener for the view @JvmField - protected var onClickListener: OnClickListener? = crSwitchBuilder.onClickListener + protected var onClickListener: View.OnClickListener? = crSwitchBuilder.onClickListener // Handler for the main looper @JvmField @@ -113,7 +119,7 @@ abstract class CircularRevealSwitch>(crSwitchBuilder: T) Log.d( TAG, "init -> " + "activity: ${activity.get()}, " + - "window: ${window.get()}, " + + "window: ${window}, " + "decorView: $decorView" ) } @@ -127,6 +133,8 @@ abstract class CircularRevealSwitch>(crSwitchBuilder: T) override fun onClick(v: View) { + if (!isViewClickable) return + application.registerActivityLifecycleCallbacks(CRActivityLifecycleCallback) onClickListener?.onClick(v) } @@ -191,34 +199,36 @@ abstract class CircularRevealSwitch>(crSwitchBuilder: T) } } - createShrinkAnimator( - iv, - x.toInt(), y.toInt(), - radius, 0F - ).apply { - interpolator = this@CircularRevealSwitch.interpolator - duration = this@CircularRevealSwitch.duration - addListener(onStart = { - isViewClickable = false - onShrinkListener?.onAnimStart() - }, onEnd = { - isViewClickable = true - decorView.removeView(iv) - onShrinkListener?.onAnimEnd() - if (DEBUG) { - Log.d( - TAG, "shrink-end -> " + - "activity: ${activity.get()}, " + - "window: ${window.get()}, " + - "decorView: $decorView" - ) - } - }, onCancel = { - isViewClickable = true - decorView.removeView(iv) - onShrinkListener?.onAnimCancel() - }) - }.start() + iv.doOnAttach { view -> + createShrinkAnimator( + view, + x.toInt(), y.toInt(), + radius, 0F + ).apply { + interpolator = this@CircularRevealSwitch.interpolator + duration = this@CircularRevealSwitch.duration + addListener(onStart = { + isViewClickable = false + onShrinkListener?.onAnimStart() + }, onEnd = { + isViewClickable = true + decorView.removeView(view) + onShrinkListener?.onAnimEnd() + if (DEBUG) { + Log.d( + TAG, "shrink-end -> " + + "activity: ${activity.get()}, " + + "window: ${window}, " + + "decorView: $decorView" + ) + } + }, onCancel = { + isViewClickable = true + decorView.removeView(view) + onShrinkListener?.onAnimCancel() + }) + }.start() + } } /** @@ -240,35 +250,38 @@ abstract class CircularRevealSwitch>(crSwitchBuilder: T) val content = decorView.findViewById(android.R.id.content) content.isInvisible = true - createExpandAnimator( - content, - x.toInt(), y.toInt(), - 0F, radius - ).apply { - interpolator = this@CircularRevealSwitch.interpolator - duration = this@CircularRevealSwitch.duration - addListener(onStart = { - isViewClickable = false - content.isInvisible = false - onExpandListener?.onAnimStart() - }, onEnd = { - isViewClickable = true - decorView.removeView(iv) - onExpandListener?.onAnimEnd() - if (DEBUG) { - Log.d( - TAG, "expand-end -> " + - "activity: ${activity.get()}, " + - "window: ${window.get()}, " + - "decorView: $decorView" - ) - } - }, onCancel = { - isViewClickable = true - decorView.removeView(iv) - onExpandListener?.onAnimCancel() - }) - }.start() + + content.doOnAttach { view -> + createExpandAnimator( + view, + x.toInt(), y.toInt(), + 0F, radius + ).apply { + interpolator = this@CircularRevealSwitch.interpolator + duration = this@CircularRevealSwitch.duration + addListener(onStart = { + isViewClickable = false + view.isInvisible = false + onExpandListener?.onAnimStart() + }, onEnd = { + isViewClickable = true + decorView.removeView(iv) + onExpandListener?.onAnimEnd() + if (DEBUG) { + Log.d( + TAG, "expand-end -> " + + "activity: ${activity.get()}, " + + "window: ${window}, " + + "decorView: $decorView" + ) + } + }, onCancel = { + isViewClickable = true + decorView.removeView(iv) + onExpandListener?.onAnimCancel() + }) + }.start() + } } /** @@ -283,7 +296,7 @@ abstract class CircularRevealSwitch>(crSwitchBuilder: T) Log.d( TAG, "calcRadius -> " + "activity: ${activity.get()}, " + - "window: ${window.get()}, " + + "window: ${window}, " + "decorView: $decorView" ) } @@ -347,6 +360,40 @@ abstract class CircularRevealSwitch>(crSwitchBuilder: T) ) } + /** + * This method is used to reassign the activity, window, and decorView. + * After each recreate, a new Activity instance is generated. + * By using ActivityLifecycleCallback, we can get the new Activity instance and reassign it, + * thus obtaining the information of the new Activity. + * + * Some device models do not support the reuse of DecorView, + * so this method can be seen as a compromise. + */ + protected open fun reassignActivity() { + CRActivityLifecycleCallback.currentActivity?.let { + this.activity = it + this.window = activity.get()!!.window + this.decorView = window.decorView as ViewGroup + } + CRActivityLifecycleCallback.currentActivity = null + } + + /** + * This method is used to post a Runnable to the Handler's message queue. + * It is a compatibility method to ensure that the Runnable is correctly executed in the message queue in order. + * ActivityCompat.recreate in API level 27 and below will have a double layer of handler post, + * so it needs to be wrapped twice to be correctly executed in the message queue in order. + * + * @param runnable The Runnable to be added to the message queue. + */ + protected open fun Handler.postCompat(runnable: Runnable) { + if (Build.VERSION.SDK_INT >= 28) { + post(runnable) + } else { + post { post(runnable) } + } + } + /** * This function is used to clear the listeners attached to the view. * It sets the onTouchListener and onClickListener of the view to null. diff --git a/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/impl/DayNightModeCRSwitch.kt b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/impl/DayNightModeCRSwitch.kt index 7411ec0..8c8ff02 100644 --- a/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/impl/DayNightModeCRSwitch.kt +++ b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/impl/DayNightModeCRSwitch.kt @@ -89,15 +89,15 @@ open class DayNightModeCRSwitch(builder: Builder) : if (DEBUG) { Log.d(TAG, "onDayNightModeClick") } - if (!isViewClickable) return - val screenshot = window.get()!!.screenshot() + val screenshot = window.screenshot() toNightMode = !isNightMode AppCompatDelegate.setDefaultNightMode( if (toNightMode) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO ) - handler.post { + handler.postCompat { + reassignActivity() animateDayNightMode(screenshot) } } diff --git a/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/impl/ThemeCRSwitch.kt b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/impl/ThemeCRSwitch.kt index 4db00b3..382125b 100644 --- a/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/impl/ThemeCRSwitch.kt +++ b/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/impl/ThemeCRSwitch.kt @@ -90,13 +90,12 @@ open class ThemeCRSwitch(builder: Builder) : * @param v The view that was clicked. */ protected open fun onThemeClick(v: View) { - if (!isViewClickable) return - - val screenshot = window.get()!!.screenshot() + val screenshot = window.screenshot() if (newTheme == toTheme) return newTheme = toTheme ActivityCompat.recreate(activity.get()!!) - handler.post { + handler.postCompat { + reassignActivity() animateTheme(screenshot) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76d9541..9c8c58f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,12 @@ [versions] agp = "8.3.2" kotlin = "1.9.23" -coreKtx = "1.13.0" +coreKtx = "1.13.1" junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1" appcompat = "1.6.1" -material = "1.11.0" +material = "1.12.0" constraintlayout = "2.1.4" lifecycleLivedataKtx = "2.7.0" lifecycleViewmodelKtx = "2.7.0"