Commit 0b82198c authored by Mygod's avatar Mygod Committed by Mygod

Add progress indicator to ServiceButton

parent 8c2686a2
......@@ -149,6 +149,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}
fab = findViewById(R.id.fab)
fab.initProgress(findViewById(R.id.fabProgress))
fab.setOnClickListener { toggle() }
ViewCompat.setOnApplyWindowInsetsListener(fab) { view, insets ->
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
......
/*******************************************************************************
* *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.progressindicator.CircularProgressIndicator
class FabProgressBehavior(context: Context, attrs: AttributeSet?) :
CoordinatorLayout.Behavior<CircularProgressIndicator>(context, attrs) {
override fun layoutDependsOn(parent: CoordinatorLayout, child: CircularProgressIndicator, dependency: View) =
dependency.id == (child.layoutParams as CoordinatorLayout.LayoutParams).anchorId
override fun onLayoutChild(parent: CoordinatorLayout, child: CircularProgressIndicator,
layoutDirection: Int): Boolean {
val size = parent.getDependencies(child).single().measuredHeight + child.trackThickness
return if (child.indicatorSize != size) {
child.indicatorSize = size
true
} else false
}
}
......@@ -28,41 +28,69 @@ import android.view.PointerIcon
import android.view.View
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.TooltipCompat
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.github.shadowsocks.R
import com.github.shadowsocks.bg.BaseService
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.progressindicator.BaseProgressIndicator
import com.google.android.material.progressindicator.DeterminateDrawable
import java.util.*
class ServiceButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
FloatingActionButton(context, attrs, defStyleAttr) {
FloatingActionButton(context, attrs, defStyleAttr), DynamicAnimation.OnAnimationEndListener {
companion object {
private val springAnimator by lazy {
DeterminateDrawable::class.java.getDeclaredField("springAnimator").apply { isAccessible = true }
}
}
private val callback = object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable) {
super.onAnimationEnd(drawable)
var next = animationQueue.peek() ?: return
if (next.current == drawable) {
if (next.icon.current == drawable) {
animationQueue.pop()
next = animationQueue.peek() ?: return
}
setImageDrawable(next)
next.start()
}
}
private fun createIcon(@DrawableRes resId: Int): AnimatedVectorDrawableCompat {
val result = AnimatedVectorDrawableCompat.create(context, resId)!!
result.registerAnimationCallback(callback)
return result
private inner class AnimatedState(@DrawableRes resId: Int,
private val onStart: BaseProgressIndicator<*>.() -> Unit = {
hide()
isIndeterminate = true
show()
}) {
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 { createIcon(R.drawable.ic_service_stopped) }
private val iconConnecting by lazy { createIcon(R.drawable.ic_service_connecting) }
private val iconConnected by lazy { createIcon(R.drawable.ic_service_connected) }
private val iconStopping by lazy { createIcon(R.drawable.ic_service_stopping) }
private val animationQueue = ArrayDeque<AnimatedVectorDrawableCompat>()
private val iconStopped by lazy { AnimatedState(R.drawable.ic_service_stopped) { hide() } }
private val iconConnecting by lazy { AnimatedState(R.drawable.ic_service_connecting) }
private val iconConnected by lazy { AnimatedState(R.drawable.ic_service_connected) { setProgressCompat(1, true) } }
private val iconStopping by lazy { AnimatedState(R.drawable.ic_service_stopping) }
private val animationQueue = ArrayDeque<AnimatedState>()
private var checked = false
private lateinit var progress: BaseProgressIndicator<*>
fun initProgress(progress: BaseProgressIndicator<*>) {
this.progress = progress
(springAnimator.get(progress.progressDrawable) as DynamicAnimation<*>).addEndListener(this)
}
override fun onAnimationEnd(animation: DynamicAnimation<out DynamicAnimation<*>>?, canceled: Boolean, value: Float,
velocity: Float) {
if (!canceled) progress.hide()
}
override fun onCreateDrawableState(extraSpace: Int): IntArray {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
......@@ -90,8 +118,8 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
if (enabled) PointerIcon.TYPE_HAND else PointerIcon.TYPE_WAIT)
}
private fun changeState(icon: AnimatedVectorDrawableCompat, animate: Boolean) {
fun counters(a: AnimatedVectorDrawableCompat, b: AnimatedVectorDrawableCompat): Boolean =
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 ||
......@@ -99,15 +127,11 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
if (animate) {
if (animationQueue.size < 2 || !counters(animationQueue.last, icon)) {
animationQueue.add(icon)
if (animationQueue.size == 1) {
setImageDrawable(icon)
icon.start()
}
if (animationQueue.size == 1) icon.start()
} else animationQueue.removeLast()
} else {
animationQueue.peekFirst()?.stop()
animationQueue.clear()
setImageDrawable(icon)
icon.start() // force ensureAnimatorSet to be called so that stop() will work
icon.stop()
}
......
......@@ -20,6 +20,20 @@
android:layout_height="match_parent"
android:fitsSystemWindows="true"/>
<!-- We double trackThickness as half of it will be invisible -->
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/fabProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:max="1"
android:visibility="invisible"
app:layout_anchor="@+id/fab"
app:layout_anchorGravity="center"
app:layout_behavior=".widget.FabProgressBehavior"
app:indicatorColor="@color/material_accent_200"
app:trackThickness="8dp"
app:trackCornerRadius="@dimen/mtrl_progress_track_thickness"/>
<com.github.shadowsocks.widget.ServiceButton
android:id="@+id/fab"
android:layout_width="wrap_content"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment