Skip to content

Commit 0b82198

Browse files
committed
Add progress indicator to ServiceButton
1 parent 8c2686a commit 0b82198

4 files changed

Lines changed: 100 additions & 19 deletions

File tree

mobile/src/main/java/com/github/shadowsocks/MainActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
149149
}
150150

151151
fab = findViewById(R.id.fab)
152+
fab.initProgress(findViewById(R.id.fabProgress))
152153
fab.setOnClickListener { toggle() }
153154
ViewCompat.setOnApplyWindowInsetsListener(fab) { view, insets ->
154155
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*******************************************************************************
2+
* *
3+
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
4+
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
5+
* *
6+
* This program is free software: you can redistribute it and/or modify *
7+
* it under the terms of the GNU General Public License as published by *
8+
* the Free Software Foundation, either version 3 of the License, or *
9+
* (at your option) any later version. *
10+
* *
11+
* This program is distributed in the hope that it will be useful, *
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14+
* GNU General Public License for more details. *
15+
* *
16+
* You should have received a copy of the GNU General Public License *
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
18+
* *
19+
*******************************************************************************/
20+
21+
package com.github.shadowsocks.widget
22+
23+
import android.content.Context
24+
import android.util.AttributeSet
25+
import android.view.View
26+
import androidx.coordinatorlayout.widget.CoordinatorLayout
27+
import com.google.android.material.progressindicator.CircularProgressIndicator
28+
29+
class FabProgressBehavior(context: Context, attrs: AttributeSet?) :
30+
CoordinatorLayout.Behavior<CircularProgressIndicator>(context, attrs) {
31+
override fun layoutDependsOn(parent: CoordinatorLayout, child: CircularProgressIndicator, dependency: View) =
32+
dependency.id == (child.layoutParams as CoordinatorLayout.LayoutParams).anchorId
33+
34+
override fun onLayoutChild(parent: CoordinatorLayout, child: CircularProgressIndicator,
35+
layoutDirection: Int): Boolean {
36+
val size = parent.getDependencies(child).single().measuredHeight + child.trackThickness
37+
return if (child.indicatorSize != size) {
38+
child.indicatorSize = size
39+
true
40+
} else false
41+
}
42+
}

mobile/src/main/java/com/github/shadowsocks/widget/ServiceButton.kt

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,41 +28,69 @@ import android.view.PointerIcon
2828
import android.view.View
2929
import androidx.annotation.DrawableRes
3030
import androidx.appcompat.widget.TooltipCompat
31+
import androidx.dynamicanimation.animation.DynamicAnimation
3132
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
3233
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
3334
import com.github.shadowsocks.R
3435
import com.github.shadowsocks.bg.BaseService
3536
import com.google.android.material.floatingactionbutton.FloatingActionButton
37+
import com.google.android.material.progressindicator.BaseProgressIndicator
38+
import com.google.android.material.progressindicator.DeterminateDrawable
3639
import java.util.*
3740

3841
class ServiceButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
39-
FloatingActionButton(context, attrs, defStyleAttr) {
42+
FloatingActionButton(context, attrs, defStyleAttr), DynamicAnimation.OnAnimationEndListener {
43+
companion object {
44+
private val springAnimator by lazy {
45+
DeterminateDrawable::class.java.getDeclaredField("springAnimator").apply { isAccessible = true }
46+
}
47+
}
48+
4049
private val callback = object : Animatable2Compat.AnimationCallback() {
4150
override fun onAnimationEnd(drawable: Drawable) {
4251
super.onAnimationEnd(drawable)
4352
var next = animationQueue.peek() ?: return
44-
if (next.current == drawable) {
53+
if (next.icon.current == drawable) {
4554
animationQueue.pop()
4655
next = animationQueue.peek() ?: return
4756
}
48-
setImageDrawable(next)
4957
next.start()
5058
}
5159
}
5260

53-
private fun createIcon(@DrawableRes resId: Int): AnimatedVectorDrawableCompat {
54-
val result = AnimatedVectorDrawableCompat.create(context, resId)!!
55-
result.registerAnimationCallback(callback)
56-
return result
61+
private inner class AnimatedState(@DrawableRes resId: Int,
62+
private val onStart: BaseProgressIndicator<*>.() -> Unit = {
63+
hide()
64+
isIndeterminate = true
65+
show()
66+
}) {
67+
val icon: AnimatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(context, resId)!!.apply {
68+
registerAnimationCallback(this@ServiceButton.callback)
69+
}
70+
fun start() {
71+
setImageDrawable(icon)
72+
icon.start()
73+
progress.onStart()
74+
}
75+
fun stop() = icon.stop()
5776
}
5877

59-
private val iconStopped by lazy { createIcon(R.drawable.ic_service_stopped) }
60-
private val iconConnecting by lazy { createIcon(R.drawable.ic_service_connecting) }
61-
private val iconConnected by lazy { createIcon(R.drawable.ic_service_connected) }
62-
private val iconStopping by lazy { createIcon(R.drawable.ic_service_stopping) }
63-
private val animationQueue = ArrayDeque<AnimatedVectorDrawableCompat>()
78+
private val iconStopped by lazy { AnimatedState(R.drawable.ic_service_stopped) { hide() } }
79+
private val iconConnecting by lazy { AnimatedState(R.drawable.ic_service_connecting) }
80+
private val iconConnected by lazy { AnimatedState(R.drawable.ic_service_connected) { setProgressCompat(1, true) } }
81+
private val iconStopping by lazy { AnimatedState(R.drawable.ic_service_stopping) }
82+
private val animationQueue = ArrayDeque<AnimatedState>()
6483

6584
private var checked = false
85+
private lateinit var progress: BaseProgressIndicator<*>
86+
fun initProgress(progress: BaseProgressIndicator<*>) {
87+
this.progress = progress
88+
(springAnimator.get(progress.progressDrawable) as DynamicAnimation<*>).addEndListener(this)
89+
}
90+
override fun onAnimationEnd(animation: DynamicAnimation<out DynamicAnimation<*>>?, canceled: Boolean, value: Float,
91+
velocity: Float) {
92+
if (!canceled) progress.hide()
93+
}
6694

6795
override fun onCreateDrawableState(extraSpace: Int): IntArray {
6896
val drawableState = super.onCreateDrawableState(extraSpace + 1)
@@ -90,24 +118,20 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
90118
if (enabled) PointerIcon.TYPE_HAND else PointerIcon.TYPE_WAIT)
91119
}
92120

93-
private fun changeState(icon: AnimatedVectorDrawableCompat, animate: Boolean) {
94-
fun counters(a: AnimatedVectorDrawableCompat, b: AnimatedVectorDrawableCompat): Boolean =
121+
private fun changeState(icon: AnimatedState, animate: Boolean) {
122+
fun counters(a: AnimatedState, b: AnimatedState): Boolean =
95123
a == iconStopped && b == iconConnecting ||
96124
a == iconConnecting && b == iconStopped ||
97125
a == iconConnected && b == iconStopping ||
98126
a == iconStopping && b == iconConnected
99127
if (animate) {
100128
if (animationQueue.size < 2 || !counters(animationQueue.last, icon)) {
101129
animationQueue.add(icon)
102-
if (animationQueue.size == 1) {
103-
setImageDrawable(icon)
104-
icon.start()
105-
}
130+
if (animationQueue.size == 1) icon.start()
106131
} else animationQueue.removeLast()
107132
} else {
108133
animationQueue.peekFirst()?.stop()
109134
animationQueue.clear()
110-
setImageDrawable(icon)
111135
icon.start() // force ensureAnimatorSet to be called so that stop() will work
112136
icon.stop()
113137
}

mobile/src/main/res/layout/layout_main.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@
2020
android:layout_height="match_parent"
2121
android:fitsSystemWindows="true"/>
2222

23+
<!-- We double trackThickness as half of it will be invisible -->
24+
<com.google.android.material.progressindicator.CircularProgressIndicator
25+
android:id="@+id/fabProgress"
26+
android:layout_width="wrap_content"
27+
android:layout_height="wrap_content"
28+
android:elevation="6dp"
29+
android:max="1"
30+
android:visibility="invisible"
31+
app:layout_anchor="@+id/fab"
32+
app:layout_anchorGravity="center"
33+
app:layout_behavior=".widget.FabProgressBehavior"
34+
app:indicatorColor="@color/material_accent_200"
35+
app:trackThickness="8dp"
36+
app:trackCornerRadius="@dimen/mtrl_progress_track_thickness"/>
2337
<com.github.shadowsocks.widget.ServiceButton
2438
android:id="@+id/fab"
2539
android:layout_width="wrap_content"

0 commit comments

Comments
 (0)