funkwhale-app-android/app/src/main/java/audio/funkwhale/ffa/views/NowPlayingView.kt

256 lines
7.0 KiB
Kotlin
Raw Normal View History

package audio.funkwhale.ffa.views
2019-08-19 16:50:33 +02:00
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.GestureDetector
2021-07-16 10:03:52 +02:00
import android.view.LayoutInflater
2019-08-19 16:50:33 +02:00
import android.view.MotionEvent
import android.view.View
import android.view.ViewTreeObserver
import android.view.animation.DecelerateInterpolator
import audio.funkwhale.ffa.R
2021-07-16 10:03:52 +02:00
import audio.funkwhale.ffa.databinding.PartialNowPlayingBinding
2019-08-19 16:50:33 +02:00
import com.google.android.material.card.MaterialCardView
import kotlin.math.abs
import kotlin.math.min
class NowPlayingView : MaterialCardView {
val activity: Context
var gestureDetector: GestureDetector? = null
var gestureDetectorCallback: OnGestureDetection? = null
2021-07-16 10:03:52 +02:00
private val binding =
PartialNowPlayingBinding.inflate(LayoutInflater.from(context), this, true)
2019-08-19 16:50:33 +02:00
constructor(context: Context) : super(context) {
activity = context
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
activity = context
}
constructor(context: Context, attrs: AttributeSet?, style: Int) : super(context, attrs, style) {
activity = context
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
2021-07-16 10:03:52 +02:00
binding.nowPlayingRoot.measure(
widthMeasureSpec,
MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED)
)
2019-08-19 16:50:33 +02:00
}
override fun onVisibilityChanged(changedView: View, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
if (visibility == View.VISIBLE && gestureDetector == null) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
gestureDetectorCallback = OnGestureDetection()
gestureDetector = GestureDetector(context, gestureDetectorCallback!!)
2019-08-19 16:50:33 +02:00
setOnTouchListener { _, motionEvent ->
val ret = gestureDetector?.onTouchEvent(motionEvent) ?: false
if (motionEvent.actionMasked == MotionEvent.ACTION_UP) {
if (gestureDetectorCallback?.isScrolling == true) {
gestureDetectorCallback?.onUp()
2019-08-19 16:50:33 +02:00
}
}
2021-07-16 10:03:52 +02:00
performClick()
2019-08-19 16:50:33 +02:00
ret
}
viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})
}
}
fun isOpened(): Boolean = gestureDetectorCallback?.isOpened() ?: false
fun close() {
gestureDetectorCallback?.close()
}
inner class OnGestureDetection : GestureDetector.SimpleOnGestureListener() {
private var maxHeight = 0
2019-08-19 16:50:33 +02:00
private var minHeight = 0
private var maxMargin = 0
private var initialTouchY = 0f
private var lastTouchY = 0f
var isScrolling = false
private var flingAnimator: ValueAnimator? = null
init {
(layoutParams as? MarginLayoutParams)?.let {
maxMargin = it.marginStart
}
minHeight = TypedValue().let {
activity.theme.resolveAttribute(R.attr.actionBarSize, it, true)
TypedValue.complexToDimensionPixelSize(it.data, resources.displayMetrics)
}
2021-07-16 10:03:52 +02:00
maxHeight = binding.nowPlayingDetails.measuredHeight + (2 * maxMargin)
2019-08-19 16:50:33 +02:00
}
override fun onDown(e: MotionEvent): Boolean {
initialTouchY = e.rawY
lastTouchY = e.rawY
return true
}
fun onUp(): Boolean {
2019-08-19 16:50:33 +02:00
isScrolling = false
layoutParams.let {
val offsetToMax = maxHeight - height
val offsetToMin = height - minHeight
flingAnimator =
if (offsetToMin < offsetToMax) ValueAnimator.ofInt(it.height, minHeight)
else ValueAnimator.ofInt(it.height, maxHeight)
animateFling(500)
return true
}
}
2021-07-16 10:03:52 +02:00
override fun onFling(
firstMotionEvent: MotionEvent,
secondMotionEvent: MotionEvent,
2021-07-16 10:03:52 +02:00
velocityX: Float,
velocityY: Float
): Boolean {
2019-08-19 16:50:33 +02:00
isScrolling = false
layoutParams.let {
val diff =
if (velocityY < 0) maxHeight - it.height
else it.height - minHeight
flingAnimator =
if (velocityY < 0) ValueAnimator.ofInt(it.height, maxHeight)
else ValueAnimator.ofInt(it.height, minHeight)
animateFling(min(abs((diff.toFloat() / velocityY * 1000).toLong()), 600))
}
return true
}
2021-07-16 10:03:52 +02:00
override fun onScroll(
firstMotionEvent: MotionEvent,
secondMotionEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
2019-08-19 16:50:33 +02:00
isScrolling = true
layoutParams.let {
val newHeight = it.height + lastTouchY - secondMotionEvent.rawY
val progress = (newHeight - minHeight) / (maxHeight - minHeight)
val newMargin = maxMargin - (maxMargin * progress)
2021-07-16 10:03:52 +02:00
(layoutParams as? MarginLayoutParams)?.let { params ->
params.marginStart = newMargin.toInt()
params.marginEnd = newMargin.toInt()
params.bottomMargin = newMargin.toInt()
2019-08-19 16:50:33 +02:00
}
layoutParams = layoutParams.apply {
when {
newHeight <= minHeight -> {
height = minHeight
return true
}
newHeight >= maxHeight -> {
height = maxHeight
return true
}
else -> height = newHeight.toInt()
}
}
2021-07-16 10:03:52 +02:00
binding.summary.alpha = 1f - progress
2019-08-19 16:50:33 +02:00
2021-07-16 10:03:52 +02:00
binding.summary.layoutParams = binding.summary.layoutParams.apply {
2019-08-19 16:50:33 +02:00
height = (minHeight * (1f - progress)).toInt()
}
}
lastTouchY = secondMotionEvent.rawY
return true
}
override fun onSingleTapUp(e: MotionEvent): Boolean {
2019-08-19 16:50:33 +02:00
layoutParams.let {
if (height != minHeight) return true
flingAnimator = ValueAnimator.ofInt(it.height, maxHeight)
animateFling(300)
}
return true
}
fun isOpened(): Boolean = layoutParams.height == maxHeight
fun close(): Boolean {
layoutParams.let {
if (it.height == minHeight) return true
flingAnimator = ValueAnimator.ofInt(it.height, minHeight)
animateFling(300)
}
return true
}
private fun animateFling(dur: Long) {
flingAnimator?.apply {
duration = dur
interpolator = DecelerateInterpolator()
addUpdateListener { valueAnimator ->
layoutParams = layoutParams.apply {
val newHeight = valueAnimator.animatedValue as Int
val progress = (newHeight.toFloat() - minHeight) / (maxHeight - minHeight)
val newMargin = maxMargin - (maxMargin * progress)
(layoutParams as? MarginLayoutParams)?.let {
it.marginStart = newMargin.toInt()
it.marginEnd = newMargin.toInt()
it.bottomMargin = newMargin.toInt()
}
height = newHeight
2021-07-16 10:03:52 +02:00
binding.summary.alpha = 1f - progress
2019-08-19 16:50:33 +02:00
2021-07-16 10:03:52 +02:00
binding.summary.layoutParams = binding.summary.layoutParams.apply {
2019-08-19 16:50:33 +02:00
height = (minHeight * (1f - progress)).toInt()
}
}
}
start()
}
}
}
}