forked from starifly/NekoBoxForAndroid
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathServiceButton.kt
More file actions
166 lines (152 loc) · 6.45 KB
/
Copy pathServiceButton.kt
File metadata and controls
166 lines (152 loc) · 6.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package io.nekohasekai.sagernet.widget
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.AttributeSet
import android.view.PointerIcon
import android.view.View
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.TooltipCompat
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.progressindicator.BaseProgressIndicator
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.bg.BaseService
import io.nekohasekai.sagernet.ktx.getColorAttr
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
class ServiceButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) :
FloatingActionButton(context, attrs, defStyleAttr), DynamicAnimation.OnAnimationEndListener {
private val callback = object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable) {
super.onAnimationEnd(drawable)
var next = animationQueue.peek() ?: return
if (next.icon.current == drawable) {
animationQueue.pop()
next = animationQueue.peek() ?: return
}
next.start()
}
}
private inner class AnimatedState(
@DrawableRes resId: Int,
private val onStart: BaseProgressIndicator<*>.() -> Unit = { hideProgress() }
) {
val icon: AnimatedVectorDrawableCompat =
AnimatedVectorDrawableCompat.create(context, resId)!!.apply {
registerAnimationCallback(this@ServiceButton.callback)
}
fun start() {
setImageDrawable(icon)
icon.start()
progress.onStart()
}
fun stop() = icon.stop()
}
private val iconStopped by lazy { AnimatedState(R.drawable.ic_service_stopped) }
private val iconConnecting by lazy {
AnimatedState(R.drawable.ic_service_connecting) {
hideProgress()
delayedAnimation = (context as LifecycleOwner).lifecycleScope.launch {
delay(context.resources.getInteger(android.R.integer.config_mediumAnimTime) + 1000L)
isIndeterminate = true
show()
}
}
}
private val iconConnected by lazy {
AnimatedState(R.drawable.ic_service_connected) {
delayedAnimation?.cancel()
setProgressCompat(1, true)
}
}
private val iconStopping by lazy { AnimatedState(R.drawable.ic_service_stopping) }
private val animationQueue = ArrayDeque<AnimatedState>()
private var checked = false
private var delayedAnimation: Job? = null
private lateinit var progress: BaseProgressIndicator<*>
fun initProgress(progress: BaseProgressIndicator<*>) {
this.progress = progress
progress.progressDrawable?.addSpringAnimationEndListener(this)
}
override fun onAnimationEnd(
animation: DynamicAnimation<out DynamicAnimation<*>>?, canceled: Boolean, value: Float,
velocity: Float
) {
if (!canceled) progress.hide()
}
private fun hideProgress() {
delayedAnimation?.cancel()
progress.hide()
}
override fun onCreateDrawableState(extraSpace: Int): IntArray {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
if (checked) View.mergeDrawableStates(
drawableState,
intArrayOf(android.R.attr.state_checked)
)
return drawableState
}
fun changeState(state: BaseService.State, previousState: BaseService.State, animate: Boolean) {
when (state) {
BaseService.State.Connecting -> changeState(iconConnecting, animate)
BaseService.State.Connected -> changeState(iconConnected, animate)
BaseService.State.Stopping -> {
changeState(iconStopping, animate && previousState == BaseService.State.Connected)
}
else -> changeState(iconStopped, animate)
}
checked = state == BaseService.State.Connected
refreshDrawableState()
applyStateTint(state)
val description = context.getText(if (state.canStop) R.string.stop else R.string.connect)
contentDescription = description
TooltipCompat.setTooltipText(this, description)
val enabled = state.canStop || state == BaseService.State.Stopped
isEnabled = enabled
if (Build.VERSION.SDK_INT >= 24) pointerIcon = PointerIcon.getSystemIcon(
context,
if (enabled) PointerIcon.TYPE_HAND else PointerIcon.TYPE_WAIT
)
}
private fun applyStateTint(state: BaseService.State) {
// Tint the connect FAB icon by state: connected=green, stopped=red. For
// transient states (and on non-Dracula themes) fall back to colorOnPrimary,
// which is the FAB's normal icon color, so those themes look unchanged.
val attr = when (state) {
BaseService.State.Connected -> R.attr.statusConnectedColor
BaseService.State.Stopped -> R.attr.fabStoppedColor
else -> com.google.android.material.R.attr.colorOnPrimary
}
imageTintList = ColorStateList.valueOf(context.getColorAttr(attr))
}
private fun changeState(icon: AnimatedState, animate: Boolean) {
fun counters(a: AnimatedState, b: AnimatedState): Boolean =
a == iconStopped && b == iconConnecting ||
a == iconConnecting && b == iconStopped ||
a == iconConnected && b == iconStopping ||
a == iconStopping && b == iconConnected
if (animate) {
if (animationQueue.size < 2 || !counters(animationQueue.last, icon)) {
animationQueue.add(icon)
if (animationQueue.size == 1) icon.start()
} else animationQueue.removeLast()
} else {
animationQueue.peekFirst()?.stop()
animationQueue.clear()
icon.start() // force ensureAnimatorSet to be called so that stop() will work
icon.stop()
}
}
}