DroidFS/app/src/main/java/sushi/hardcore/droidfs/widgets/CircleClipTapView.kt

198 lines
4.9 KiB
Kotlin

package sushi.hardcore.droidfs.widgets
import android.animation.Animator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import sushi.hardcore.droidfs.R
/**
* View class
*
* (Borrowed from https://github.com/vkay94/DoubleTapPlayerView)
*
* Draws a arc shape and provides a circle scaling animation.
* Used by [DoubleTapOverlay][sushi.hardcore.droidfs.widgets.DoubleTapOverlay].
*/
internal class CircleClipTapView(context: Context, attrs: AttributeSet): View(context, attrs) {
private var backgroundPaint = Paint()
private var circlePaint = Paint()
private var widthPx = 0
private var heightPx = 0
// Background
private var shapePath = Path()
private var isLeft = true
// Circle
private var cX = 0f
private var cY = 0f
private var currentRadius = 0f
private var minRadius: Int = 0
private var maxRadius: Int = 0
// Animation
private var valueAnimator: ValueAnimator? = null
private var forceReset = false
init {
backgroundPaint.apply {
style = Paint.Style.FILL
isAntiAlias = true
color = ContextCompat.getColor(context, R.color.playerDoubleTapBackground)
}
circlePaint.apply {
style = Paint.Style.FILL
isAntiAlias = true
color = ContextCompat.getColor(context, R.color.playerDoubleTapTouch)
}
// Pre-configurations depending on device display metrics
val dm = context.resources.displayMetrics
widthPx = dm.widthPixels
heightPx = dm.heightPixels
minRadius = (30f * dm.density).toInt()
maxRadius = (400f * dm.density).toInt()
updatePathShape()
valueAnimator = getCircleAnimator()
}
var performAtEnd: () -> Unit = { }
var arcSize: Float = 80f
set(value) {
field = value
updatePathShape()
}
var animationDuration: Long
get() = valueAnimator?.duration ?: 650
set(value) {
getCircleAnimator().duration = value
}
/*
Circle
*/
fun updatePosition(x: Float, y: Float) {
cX = x
cY = y
val newIsLeft = x <= resources.displayMetrics.widthPixels / 2
if (isLeft != newIsLeft) {
isLeft = newIsLeft
updatePathShape()
}
}
private fun invalidateWithCurrentRadius(factor: Float) {
currentRadius = minRadius + ((maxRadius - minRadius) * factor)
invalidate()
}
/*
Background
*/
private fun updatePathShape() {
val halfWidth = widthPx * 0.5f
shapePath.reset()
val w = if (isLeft) 0f else widthPx.toFloat()
val f = if (isLeft) 1 else -1
shapePath.moveTo(w, 0f)
shapePath.lineTo(f * (halfWidth - arcSize) + w, 0f)
shapePath.quadTo(
f * (halfWidth + arcSize) + w,
heightPx.toFloat() / 2,
f * (halfWidth - arcSize) + w,
heightPx.toFloat()
)
shapePath.lineTo(w, heightPx.toFloat())
shapePath.close()
invalidate()
}
/*
Animation
*/
private fun getCircleAnimator(): ValueAnimator {
if (valueAnimator == null) {
valueAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = animationDuration
addUpdateListener {
invalidateWithCurrentRadius(it.animatedValue as Float)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {
visibility = VISIBLE
}
override fun onAnimationEnd(animation: Animator) {
if (!forceReset) performAtEnd()
}
override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationCancel(animation: Animator) {}
})
}
}
return valueAnimator!!
}
fun resetAnimation(body: () -> Unit) {
forceReset = true
getCircleAnimator().end()
body()
forceReset = false
getCircleAnimator().start()
}
fun endAnimation() {
getCircleAnimator().end()
}
/*
Others: Drawing and Measurements
*/
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
widthPx = w
heightPx = h
updatePathShape()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Background
canvas.clipPath(shapePath)
canvas.drawPath(shapePath, backgroundPaint)
// Circle
canvas.drawCircle(cX, cY, currentRadius, circlePaint)
}
}