Skip to content

Commit

Permalink
fix: some devices cannot immediately attach view, causing animation t…
Browse files Browse the repository at this point in the history
…o fail

fix: animation does not display on API less than 28
  • Loading branch information
YenalyLiew committed May 12, 2024
1 parent de83d39 commit 354be69
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 78 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ dependencyResolutionManagement {

```kotlin
dependencies {
implementation("com.github.YenalyLiew:CircularRevealSwitch:0.2")
implementation("com.github.YenalyLiew:CircularRevealSwitch:0.3")
}
```

Expand Down Expand Up @@ -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 时能应用到新主题,请自行储存并设置
- 本库只提供切换主题效果,不提供切换后主题的保存。如果你需要保存主题,希望下次启动软件或者打开新 Activity 时能应用到新主题,请自行储存并设置

## 更新

### v0.3

1. 修复 #1:部分手机不能立即 attach view 从而导致无法启动动画的问题
2. API 小于 28 时动画不能显示的问题
2 changes: 1 addition & 1 deletion circularrevealswitch/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ android {

afterEvaluate {
publishing {
val versionName = "0.2"
val versionName = "0.3"
publications {
create<MavenPublication>("release") {
from(components["release"])
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Activity>? = 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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ 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
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
Expand All @@ -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<T : CRSwitchBuilder<T>>(crSwitchBuilder: T) :
View.OnTouchListener, OnClickListener, LifecycleEventObserver {
View.OnTouchListener, View.OnClickListener,
LifecycleEventObserver {

companion object {
const val TAG = "CircularRevealSwitch"
Expand Down Expand Up @@ -67,21 +70,24 @@ abstract class CircularRevealSwitch<T : CRSwitchBuilder<T>>(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<Activity> = context.get()!!.activity.weak()
protected var activity: WeakReference<Activity> = context.get()!!.activity.weak()

// The window of the activity
@JvmField
protected val window: WeakReference<Window> = 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
Expand Down Expand Up @@ -113,7 +119,7 @@ abstract class CircularRevealSwitch<T : CRSwitchBuilder<T>>(crSwitchBuilder: T)
Log.d(
TAG, "init -> " +
"activity: ${activity.get()}, " +
"window: ${window.get()}, " +
"window: ${window}, " +
"decorView: $decorView"
)
}
Expand All @@ -127,6 +133,8 @@ abstract class CircularRevealSwitch<T : CRSwitchBuilder<T>>(crSwitchBuilder: T)


override fun onClick(v: View) {
if (!isViewClickable) return
application.registerActivityLifecycleCallbacks(CRActivityLifecycleCallback)
onClickListener?.onClick(v)
}

Expand Down Expand Up @@ -191,34 +199,36 @@ abstract class CircularRevealSwitch<T : CRSwitchBuilder<T>>(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()
}
}

/**
Expand All @@ -240,35 +250,38 @@ abstract class CircularRevealSwitch<T : CRSwitchBuilder<T>>(crSwitchBuilder: T)

val content = decorView.findViewById<View>(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()
}
}

/**
Expand All @@ -283,7 +296,7 @@ abstract class CircularRevealSwitch<T : CRSwitchBuilder<T>>(crSwitchBuilder: T)
Log.d(
TAG, "calcRadius -> " +
"activity: ${activity.get()}, " +
"window: ${window.get()}, " +
"window: ${window}, " +
"decorView: $decorView"
)
}
Expand Down Expand Up @@ -347,6 +360,40 @@ abstract class CircularRevealSwitch<T : CRSwitchBuilder<T>>(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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
Loading

0 comments on commit 354be69

Please sign in to comment.