NewPipe-app-android/app/src/main/java/org/schabi/newpipe/ktx/View.kt

294 lines
10 KiB
Kotlin
Raw Normal View History

@file:JvmName("ViewUtils")
package org.schabi.newpipe.ktx
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.util.Log
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
2021-04-24 04:15:12 +02:00
import androidx.core.animation.addListener
import androidx.core.view.ViewCompat
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import org.schabi.newpipe.MainActivity
private const val TAG = "ViewUtils"
/**
* Animate the view.
*
* @param enterOrExit true to enter, false to exit
* @param duration how long the animation will take, in milliseconds
* @param animationType Type of the animation
* @param delay how long the animation will wait to start, in milliseconds
* @param execOnEnd runnable that will be executed when the animation ends
*/
@JvmOverloads
fun View.animate(
enterOrExit: Boolean,
duration: Long,
animationType: AnimationType = AnimationType.ALPHA,
delay: Long = 0,
execOnEnd: Runnable? = null
) {
if (MainActivity.DEBUG) {
val id = try {
resources.getResourceEntryName(id)
} catch (e: Exception) {
id.toString()
}
val msg = String.format(
"%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", enterOrExit,
javaClass.simpleName, id, animationType, duration, delay, execOnEnd
)
Log.d(TAG, "animate(): $msg")
}
if (isVisible && enterOrExit) {
if (MainActivity.DEBUG) {
Log.d(TAG, "animate(): view was already visible > view = [$this]")
}
animate().setListener(null).cancel()
isVisible = true
alpha = 1f
execOnEnd?.run()
return
} else if ((isGone || isInvisible) && !enterOrExit) {
if (MainActivity.DEBUG) {
Log.d(TAG, "animate(): view was already gone > view = [$this]")
}
animate().setListener(null).cancel()
isGone = true
alpha = 0f
execOnEnd?.run()
return
}
animate().setListener(null).cancel()
isVisible = true
when (animationType) {
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.LIGHT_SCALE_AND_ALPHA -> animateLightScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.SLIDE_AND_ALPHA -> animateSlideAndAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.LIGHT_SLIDE_AND_ALPHA -> animateLightSlideAndAlpha(enterOrExit, duration, delay, execOnEnd)
}
}
/**
* Animate the background color of a view.
*
* @param duration the duration of the animation
* @param colorStart the background color to start with
* @param colorEnd the background color to end with
*/
fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) {
if (MainActivity.DEBUG) {
Log.d(
TAG,
"animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
2022-07-29 22:09:02 +02:00
"colorStart = [$colorStart], colorEnd = [$colorEnd]"
)
}
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
viewPropertyAnimator.duration = duration
2022-07-29 22:09:02 +02:00
fun listenerAction(color: Int) {
ViewCompat.setBackgroundTintList(this, ColorStateList.valueOf(color))
}
2022-07-29 22:09:02 +02:00
viewPropertyAnimator.addUpdateListener { listenerAction(it.animatedValue as Int) }
viewPropertyAnimator.addListener(onCancel = { listenerAction(colorEnd) }, onEnd = { listenerAction(colorEnd) })
viewPropertyAnimator.start()
}
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
if (MainActivity.DEBUG) {
2022-07-29 22:09:02 +02:00
Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
}
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
animator.interpolator = FastOutSlowInInterpolator()
animator.duration = duration
2022-07-29 22:09:02 +02:00
fun listenerAction(value: Int) {
layoutParams.height = value
requestLayout()
}
2022-07-29 22:09:02 +02:00
animator.addUpdateListener { listenerAction((it.animatedValue as Float).toInt()) }
animator.addListener(onCancel = { listenerAction(targetHeight) }, onEnd = { listenerAction(targetHeight) })
animator.start()
return animator
}
fun View.animateRotation(duration: Long, targetRotation: Int) {
if (MainActivity.DEBUG) {
2022-07-29 22:09:02 +02:00
Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
}
animate().setListener(null).cancel()
animate()
.rotation(targetRotation.toFloat()).setDuration(duration)
.setInterpolator(FastOutSlowInInterpolator())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationCancel(animation: Animator) {
rotation = targetRotation.toFloat()
}
override fun onAnimationEnd(animation: Animator) {
rotation = targetRotation.toFloat()
}
}).start()
}
private fun View.animateAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
if (enterOrExit) {
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f)
.setDuration(duration).setStartDelay(delay)
.setListener(ExecOnEndListener(execOnEnd))
.start()
} else {
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f)
.setDuration(duration).setStartDelay(delay)
.setListener(HideAndExecOnEndListener(this, execOnEnd))
.start()
}
}
private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
if (enterOrExit) {
scaleX = .8f
scaleY = .8f
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay)
.setListener(ExecOnEndListener(execOnEnd))
.start()
} else {
scaleX = 1f
scaleY = 1f
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(duration).setStartDelay(delay)
.setListener(HideAndExecOnEndListener(this, execOnEnd))
.start()
}
}
private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
if (enterOrExit) {
alpha = .5f
scaleX = .95f
scaleY = .95f
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay)
.setListener(ExecOnEndListener(execOnEnd))
.start()
} else {
alpha = 1f
scaleX = 1f
scaleY = 1f
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.95f).scaleY(.95f)
.setDuration(duration).setStartDelay(delay)
.setListener(HideAndExecOnEndListener(this, execOnEnd))
.start()
}
}
private fun View.animateSlideAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
if (enterOrExit) {
translationY = -height.toFloat()
alpha = 0f
animate()
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
.setDuration(duration).setStartDelay(delay)
.setListener(ExecOnEndListener(execOnEnd))
.start()
} else {
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).translationY(-height.toFloat())
.setDuration(duration).setStartDelay(delay)
.setListener(HideAndExecOnEndListener(this, execOnEnd))
.start()
}
}
private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
if (enterOrExit) {
translationY = -height / 2.0f
alpha = 0f
animate()
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
.setDuration(duration).setStartDelay(delay)
.setListener(ExecOnEndListener(execOnEnd))
.start()
} else {
animate().setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).translationY(-height / 2.0f)
.setDuration(duration).setStartDelay(delay)
.setListener(HideAndExecOnEndListener(this, execOnEnd))
.start()
}
}
2022-05-13 04:16:02 +02:00
@JvmOverloads
2021-09-03 21:29:10 +02:00
fun View.slideUp(
duration: Long,
delay: Long = 0L,
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float = 1.0F,
execOnEnd: Runnable? = null
2021-09-03 21:29:10 +02:00
) {
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
animate().setListener(null).cancel()
alpha = 0f
translationY = newTranslationY.toFloat()
2021-09-03 21:29:10 +02:00
isVisible = true
animate()
.alpha(1f)
.translationY(0f)
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(FastOutSlowInInterpolator())
.setListener(ExecOnEndListener(execOnEnd))
.start()
}
/**
* Instead of hiding normally using [animate], which would make
* the recycler view unable to capture touches after being hidden, this just animates the alpha
* value setting it to `0.0` after `200` milliseconds.
*/
fun View.animateHideRecyclerViewAllowingScrolling() {
// not hiding normally because the view needs to still capture touches and allow scroll
animate().alpha(0.0f).setDuration(200).start()
}
private open class ExecOnEndListener(private val execOnEnd: Runnable?) : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}
private class HideAndExecOnEndListener(private val view: View, execOnEnd: Runnable?) :
ExecOnEndListener(execOnEnd) {
override fun onAnimationEnd(animation: Animator) {
view.isGone = true
super.onAnimationEnd(animation)
}
}
enum class AnimationType {
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
}