From d86b162d4b2408f39faafff9efdae6a3d964340a Mon Sep 17 00:00:00 2001 From: mustafadakhel <45257975+mustafadakhel@users.noreply.github.com> Date: Sat, 29 Jul 2023 16:47:36 +0300 Subject: [PATCH] feat: add double tap ripple animation (#401) * Add double tap seeking animation * Remove unnecessary formatting changes * Order imports correctly * Remove needless blank line * feat: add ripple for playback (play / pause) * refactor: clean up --------- Co-authored-by: Jarne Demeulemeester --- .../jellyfin/utils/PlayerGestureHelper.kt | 74 +++++++++++++++++-- .../src/main/res/layout/activity_player.xml | 26 +++++++ .../main/res/drawable/transparent_circle.xml | 8 ++ 3 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 core/src/main/res/drawable/transparent_circle.xml diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt index 30bd3f6f..05981bc7 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt @@ -10,9 +10,13 @@ import android.view.GestureDetector import android.view.MotionEvent import android.view.ScaleGestureDetector import android.view.View +import android.view.ViewPropertyAnimator import android.view.WindowInsets import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF +import android.view.animation.AccelerateInterpolator +import android.view.animation.DecelerateInterpolator +import android.widget.ImageView import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import dev.jdtech.jellyfin.AppPreferences @@ -72,8 +76,6 @@ class PlayerGestureHelper( val viewWidth = playerView.measuredWidth val areaWidth = viewWidth / 5 // Divide the view into 5 parts: 2:1:2 - val currentPos = playerView.player?.currentPosition ?: 0 - // Define the areas and their boundaries val leftmostAreaStart = 0 val middleAreaStart = areaWidth * 2 @@ -82,15 +84,15 @@ class PlayerGestureHelper( when (e.x.toInt()) { in leftmostAreaStart until middleAreaStart -> { // Tapped on the leftmost area (seek backward) - playerView.player?.seekTo((currentPos - appPreferences.playerSeekBackIncrement).coerceAtLeast(0)) + rewind() } in middleAreaStart until rightmostAreaStart -> { // Tapped on the middle area (toggle pause/unpause) - playerView.player?.playWhenReady = !playerView.player?.playWhenReady!! + togglePlayback() } in rightmostAreaStart until viewWidth -> { // Tapped on the rightmost area (seek forward) - playerView.player?.seekTo(currentPos + appPreferences.playerSeekForwardIncrement) + fastForward() } } return true @@ -98,6 +100,68 @@ class PlayerGestureHelper( }, ) + private fun fastForward() { + val currentPosition = playerView.player?.currentPosition ?: 0 + val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement + seekTo(fastForwardPosition) + animateRipple(activity.binding.imageFfwdAnimationRipple) + } + + private fun rewind() { + val currentPosition = playerView.player?.currentPosition ?: 0 + val rewindPosition = currentPosition - appPreferences.playerSeekBackIncrement + seekTo(rewindPosition.coerceAtLeast(0)) + animateRipple(activity.binding.imageRewindAnimationRipple) + } + + private fun togglePlayback() { + playerView.player?.playWhenReady = !playerView.player?.playWhenReady!! + animateRipple(activity.binding.imagePlaybackAnimationRipple) + } + + private fun seekTo(position: Long) { + playerView.player?.seekTo(position) + } + + private fun animateRipple(image: ImageView) { + image + .animateSeekingRippleStart() + .withEndAction { + resetRippleImage(image) + } + .start() + } + + private fun ImageView.animateSeekingRippleStart(): ViewPropertyAnimator { + val rippleImageHeight = this.height + val playerViewHeight = playerView.height.toFloat() + val playerViewWidth = playerView.width.toFloat() + val scaleDifference = playerViewHeight / rippleImageHeight + val playerViewAspectRatio = playerViewWidth / playerViewHeight + val scaleValue = scaleDifference * playerViewAspectRatio + return animate() + .alpha(1f) + .scaleX(scaleValue) + .scaleY(scaleValue) + .setDuration(180) + .setInterpolator(DecelerateInterpolator()) + } + + private fun resetRippleImage(image: ImageView) { + image + .animateSeekingRippleEnd() + .withEndAction { + image.scaleX = 1f + image.scaleY = 1f + } + .start() + } + + private fun ImageView.animateSeekingRippleEnd() = animate() + .alpha(0f) + .setDuration(150) + .setInterpolator(AccelerateInterpolator()) + private val seekGestureDetector = GestureDetector( playerView.context, object : GestureDetector.SimpleOnGestureListener() { diff --git a/app/phone/src/main/res/layout/activity_player.xml b/app/phone/src/main/res/layout/activity_player.xml index ec44e30f..ee2e8977 100644 --- a/app/phone/src/main/res/layout/activity_player.xml +++ b/app/phone/src/main/res/layout/activity_player.xml @@ -113,4 +113,30 @@ tools:ignore="ContentDescription" /> + + + + + diff --git a/core/src/main/res/drawable/transparent_circle.xml b/core/src/main/res/drawable/transparent_circle.xml new file mode 100644 index 00000000..a8e538b9 --- /dev/null +++ b/core/src/main/res/drawable/transparent_circle.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file