198 lines
5.0 KiB
Kotlin
198 lines
5.0 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)
|
||
|
}
|
||
|
}
|