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
176 lines (162 loc) · 7.02 KB
/
Copy pathServiceButton.kt
File metadata and controls
176 lines (162 loc) · 7.02 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
167
168
169
170
171
172
173
174
175
176
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.lifecycle.withStarted
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 io.nekohasekai.sagernet.ktx.unwrap
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()
val owner = context.unwrap<LifecycleOwner>()
delayedAnimation = owner.lifecycleScope.launch {
delay(context.resources.getInteger(android.R.integer.config_mediumAnimTime) + 1000L)
// Gate the UI mutation on STARTED so a delayed progress reveal doesn't run
// while the activity is stopped (the old launchWhenStarted suspended here).
owner.withStarted {
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 uses fabConnectedColor
// (defaults to the green connected status color, but themes whose FAB
// background is that same green — e.g. Dark High Contrast — override it
// with a contrasting color so the icon stays visible). Stopped uses
// fabStoppedColor. Transient states fall back to colorOnPrimary, the FAB's
// normal icon color, so unaffected themes look unchanged.
val attr = when (state) {
BaseService.State.Connected -> R.attr.fabConnectedColor
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()
}
}
}