@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 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], " + "colorStart = [$colorStart], colorEnd = [$colorEnd]" ) } val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd) viewPropertyAnimator.interpolator = FastOutSlowInInterpolator() viewPropertyAnimator.duration = duration fun listenerAction(color: Int) { ViewCompat.setBackgroundTintList(this, ColorStateList.valueOf(color)) } 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) { 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 fun listenerAction(value: Int) { layoutParams.height = value requestLayout() } 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) { 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() } } @JvmOverloads fun View.slideUp( duration: Long, delay: Long = 0L, @FloatRange(from = 0.0, to = 1.0) translationPercent: Float = 1.0F, execOnEnd: Runnable? = null ) { val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt() animate().setListener(null).cancel() alpha = 0f translationY = newTranslationY.toFloat() 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 }