Skip to content

Commit

Permalink
Add fast forward/rewind functionality
Browse files Browse the repository at this point in the history
Adds a customizable fast forward/rewind view that can be used for seeking the player

Bug: PierfrancescoSoffritti#132
  • Loading branch information
AbelTesfaye committed Mar 27, 2020
1 parent eb33709 commit 1321c8b
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.LegacyY
import com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.menu.YouTubePlayerMenu
import com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.menu.defaultMenu.DefaultYouTubePlayerMenu
import com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.utils.FadeViewHelper
import com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.views.YouTubePlayerFastForwardRewind
import com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.views.YouTubePlayerSeekBar
import com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.views.YouTubePlayerSeekBarListener

Expand Down Expand Up @@ -49,6 +50,11 @@ internal class DefaultPlayerUiController(private val youTubePlayerView: LegacyYo

private val youtubePlayerSeekBar: YouTubePlayerSeekBar

private var currentSecond = 0F

private val fastRewind: YouTubePlayerFastForwardRewind
private val fastForward: YouTubePlayerFastForwardRewind

private var onFullScreenButtonListener: View.OnClickListener
private var onMenuButtonClickListener: View.OnClickListener

Expand Down Expand Up @@ -83,11 +89,26 @@ internal class DefaultPlayerUiController(private val youTubePlayerView: LegacyYo

youtubePlayerSeekBar = controlsView.findViewById(R.id.youtube_player_seekbar)

fastRewind = controlsView.findViewById(R.id.fast_rewind_layout)
fastForward = controlsView.findViewById(R.id.fast_forward_layout)

fadeControlsContainer = FadeViewHelper(controlsContainer)

onFullScreenButtonListener = View.OnClickListener { youTubePlayerView.toggleFullScreen() }
onMenuButtonClickListener = View.OnClickListener { youTubePlayerMenu.show(menuButton) }

fastForward.addOtherFastForwardRewindView(fastRewind)
fastRewind.addOtherFastForwardRewindView(fastForward)

fastRewind.addOnSeekAction { seekBy ->
currentSecond += seekBy
youTubePlayer.seekTo(currentSecond)
}
fastForward.addOnSeekAction { seekBy ->
currentSecond += seekBy
youTubePlayer.seekTo(currentSecond)
}

initClickListeners()
}

Expand Down Expand Up @@ -293,7 +314,7 @@ internal class DefaultPlayerUiController(private val youTubePlayerView: LegacyYo
override fun onPlaybackRateChange(youTubePlayer: YouTubePlayer, playbackRate: PlayerConstants.PlaybackRate) {}
override fun onError(youTubePlayer: YouTubePlayer, error: PlayerConstants.PlayerError) {}
override fun onApiChange(youTubePlayer: YouTubePlayer) {}
override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) {}
override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) { currentSecond = second }
override fun onVideoDuration(youTubePlayer: YouTubePlayer, duration: Float) {}
override fun onVideoLoadedFraction(youTubePlayer: YouTubePlayer, loadedFraction: Float) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.views

import android.view.View
import android.view.animation.AlphaAnimation
import android.view.animation.Animation

/**
* Creates an animation that's *specifically* made for each of the triangles in the fast forward/rewind icon.
*/
internal class FastForwardRewindTrianglesAnimation(fromAlpha: Float, toAlpha: Float) : AlphaAnimation(fromAlpha, toAlpha) {

/**
* @param fromAlpha Initial alpha
* @param toAlpha Final alpha
* @param startOffset What time to start the animation. This is used to specify the current triangle animation's sequence
* @param singleTriangleAnimationDuration How long this triangle should take to fade in
* @param entireAnimationDuration How long all triangles take to execute a single animation cycle
* @param triangleView View of the triangle that's using this animation
*/
constructor(fromAlpha: Float, toAlpha: Float, startOffset: Long, singleTriangleAnimationDuration: Long, entireAnimationDuration: Long, triangleView: View) : this(fromAlpha, toAlpha) {
this.duration = singleTriangleAnimationDuration
this.startOffset = startOffset
this.initialStartOffset = startOffset
this.entireAnimationDuration = entireAnimationDuration
this.triangleView = triangleView
}

private var entireAnimationDuration = 0L
private var initialStartOffset = 0L
private lateinit var triangleView: View

init {
this.repeatCount = Animation.INFINITE
this.setAnimationListener(object : AnimationListener {
/**
* Change the startOffset so this triangle's animation loops after all triangles have
* completed a single cycle
*/
override fun onAnimationRepeat(animation: Animation?) {
animation?.startOffset = entireAnimationDuration
}

override fun onAnimationStart(animation: Animation?) {
triangleView.visibility = View.VISIBLE
}

/**
* Reset startOffset so this triangle's animation restarts properly next time
* (so it doesn't start where it was stopped)
*/
override fun onAnimationEnd(animation: Animation?) {
triangleView.visibility = View.INVISIBLE
animation?.startOffset = initialStartOffset
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.views

import android.content.Context
import android.os.CountDownTimer
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import com.pierfrancescosoffritti.androidyoutubeplayer.R


/**
* A view used for fast forwarding or fast rewinding the player.
* This view itself doesn't perform the seek event, but it exposes [addOnSeekAction] for other components
* to add callbacks that will be executed whenever a seek action occurs.
*/

class YouTubePlayerFastForwardRewind(context: Context, attrs: AttributeSet) : RelativeLayout(context, attrs) {

private val fastForwardRewindIndicator: LinearLayout

private val fastForwardRewindLeftTriangle: ImageView
private val fastForwardRewindMidImageTriangle: ImageView
private val fastForwardRewindRightImageTriangle: ImageView
private lateinit var fadeInLeftTriangleAnim: FastForwardRewindTrianglesAnimation
private lateinit var fadeInMidTriangleAnim: FastForwardRewindTrianglesAnimation
private lateinit var fadeInRightTriangleAnim: FastForwardRewindTrianglesAnimation

private val fastForwardRewindText: TextView

private val fastForwardRewindTimer: CountDownTimer
private val msFastForwardRewindWaitTime = 1000L
private var secToSeek = 0F
private var secToIncrementPerClick = 0F

private var hasBeenClickedOnce = false

private val seekActions = HashSet<(Float) -> Unit>()
private val otherFastForwardRewindViews = HashSet<YouTubePlayerFastForwardRewind>()

init {
inflate(context, R.layout.ayp_fast_forward_rewind, this)

val fastForwardRewindLayout: RelativeLayout = findViewById(R.id.fast_forward_rewind_layout)

fastForwardRewindIndicator = findViewById(R.id.fast_forward_rewind_indicator)

fastForwardRewindLeftTriangle = findViewById(R.id.fast_forward_rewind_left_image)
fastForwardRewindMidImageTriangle = findViewById(R.id.fast_forward_rewind_mid_image)
fastForwardRewindRightImageTriangle = findViewById(R.id.fast_forward_rewind_right_image)

fastForwardRewindText = findViewById(R.id.fast_forward_rewind_text)


val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.YouTubePlayerFastForwardRewind, 0, 0)
val shouldUseFastRewindLayout = typedArray.getBoolean(R.styleable.YouTubePlayerFastForwardRewind_useFastRewindLayout, false)

useFastRewindLayout(shouldUseFastRewindLayout)

fastForwardRewindTimer = object : CountDownTimer(msFastForwardRewindWaitTime, msFastForwardRewindWaitTime) {
override fun onTick(millisUntilFinished: Long) { }

/**
* Run all [seekActions] with [secToSeek] as a parameter
*/
override fun onFinish() {
// Fire seek actions only if [secToSeek] is valid. Can be invalid if timer ends after just a single click
if (secToSeek != 0F) {
for (seekAction in seekActions)
seekAction(secToSeek)
}
resetUIAndVariables()
}
}

fastForwardRewindLayout.setOnTouchListener { v, event ->
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
// Start or restart timer
fastForwardRewindTimer.cancel()
fastForwardRewindTimer.start()

// Need to stop other fastForwardRewindViews, this will avoid running multiple seekers in parallel
for (otherFastForwardRewindView in otherFastForwardRewindViews)
otherFastForwardRewindView.interruptSeek()

// If has been clicked before, then fast forward
if (hasBeenClickedOnce) {

secToSeek += secToIncrementPerClick

fastForwardRewindText.text = "${Math.abs(secToSeek.toInt())} seconds"

// Make text and triangles parent visible, then start fast forward/rewind animation
if (fastForwardRewindIndicator.visibility == View.INVISIBLE) {
fastForwardRewindIndicator.visibility = View.VISIBLE
fastForwardRewindIndicator.animate().alpha(1f).setDuration(200)

fastForwardRewindLeftTriangle.startAnimation(fadeInLeftTriangleAnim)
fastForwardRewindMidImageTriangle.startAnimation(fadeInMidTriangleAnim)
fastForwardRewindRightImageTriangle.startAnimation(fadeInRightTriangleAnim)
}
} else hasBeenClickedOnce = true
}
}
v?.onTouchEvent(event) ?: true
}
}

fun useFastRewindLayout(shouldUseFastRewindLayout: Boolean) {
val animationDurationForSingleTriangle = msFastForwardRewindWaitTime / 3

if (shouldUseFastRewindLayout) {
secToIncrementPerClick = -10F

fastForwardRewindLeftTriangle.rotation = -90f
fastForwardRewindMidImageTriangle.rotation = -90f
fastForwardRewindRightImageTriangle.rotation = -90f

fadeInRightTriangleAnim = FastForwardRewindTrianglesAnimation(0f, 1f, 0, animationDurationForSingleTriangle, msFastForwardRewindWaitTime, fastForwardRewindLeftTriangle)
fadeInMidTriangleAnim = FastForwardRewindTrianglesAnimation(0f, 1f, animationDurationForSingleTriangle / 2, animationDurationForSingleTriangle, msFastForwardRewindWaitTime, fastForwardRewindMidImageTriangle)
fadeInLeftTriangleAnim = FastForwardRewindTrianglesAnimation(0f, 1f, animationDurationForSingleTriangle, animationDurationForSingleTriangle, msFastForwardRewindWaitTime, fastForwardRewindRightImageTriangle)

} else {
secToIncrementPerClick = 10F

fastForwardRewindLeftTriangle.rotation = 90f
fastForwardRewindMidImageTriangle.rotation = 90f
fastForwardRewindRightImageTriangle.rotation = 90f

fadeInLeftTriangleAnim = FastForwardRewindTrianglesAnimation(0f, 1f, 0, animationDurationForSingleTriangle, msFastForwardRewindWaitTime, fastForwardRewindLeftTriangle)
fadeInMidTriangleAnim = FastForwardRewindTrianglesAnimation(0f, 1f, animationDurationForSingleTriangle / 2, animationDurationForSingleTriangle, msFastForwardRewindWaitTime, fastForwardRewindMidImageTriangle)
fadeInRightTriangleAnim = FastForwardRewindTrianglesAnimation(0f, 1f, animationDurationForSingleTriangle, animationDurationForSingleTriangle, msFastForwardRewindWaitTime, fastForwardRewindRightImageTriangle)
}
}

/**
* Resets the view to the way it was before any seek event was started.
* Note: This will not stop an already running seek event. If you want to
* stop an already running seek event, see [interruptSeek]
*/
fun resetUIAndVariables() {
secToSeek = 0F
hasBeenClickedOnce = false
fastForwardRewindIndicator.animate().alpha(0f).setDuration(200).withEndAction { fastForwardRewindIndicator.visibility = View.INVISIBLE }

fastForwardRewindLeftTriangle.clearAnimation()
fastForwardRewindMidImageTriangle.clearAnimation()
fastForwardRewindRightImageTriangle.clearAnimation()
}

/**
* Stops the currently running seek event then resets the view to the way it was before
* any seek event was started.
*/
fun interruptSeek() {
fastForwardRewindTimer.cancel()
resetUIAndVariables()
}

/**
* @param func Callback executed when a seek action occurs
*/
fun addOnSeekAction(func: (Float) -> Unit): Boolean = seekActions.add(func)
fun removeOnSeekAction(func: (Float) -> Unit): Boolean = seekActions.remove(func)

/**
* @param otherFastForwardRewindView Other [YouTubePlayerFastForwardRewind] that should be interrupted while using this one.
*/
fun addOtherFastForwardRewindView(otherFastForwardRewindView: YouTubePlayerFastForwardRewind): Boolean = otherFastForwardRewindViews.add(otherFastForwardRewindView)
fun removeOtherFastForwardRewindView(otherFastForwardRewindView: YouTubePlayerFastForwardRewind): Boolean = otherFastForwardRewindViews.remove(otherFastForwardRewindView)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#fff"
android:pathData="M1,24l23,0l-12,-15z"/>
</vector>
19 changes: 19 additions & 0 deletions core/src/main/res/layout/ayp_default_player_ui.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@

</LinearLayout>

<com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.views.YouTubePlayerFastForwardRewind
android:id="@+id/fast_rewind_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toStartOf="@+id/play_pause_button"

android:contentDescription="@string/ayp_fast_rewind"

app:useFastRewindLayout="true" />

<ImageView
android:id="@+id/play_pause_button"
android:layout_width="wrap_content"
Expand All @@ -97,6 +107,15 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true" />

<com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.views.YouTubePlayerFastForwardRewind
android:id="@+id/fast_forward_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"

android:contentDescription="@string/ayp_fast_forward"

android:layout_toEndOf="@+id/play_pause_button" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
Expand Down
61 changes: 61 additions & 0 deletions core/src/main/res/layout/ayp_fast_forward_rewind.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fast_forward_rewind_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"

android:background="@drawable/ayp_background_item_selected"

android:clickable="true"
android:focusable="true">

<LinearLayout
android:id="@+id/fast_forward_rewind_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

android:layout_centerInParent="true"

android:orientation="vertical"
android:visibility="invisible">

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">

<ImageView
android:id="@+id/fast_forward_rewind_left_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ayp_ic_fast_forward_rewind_triangle_16dp"
android:visibility="invisible" />

<ImageView
android:id="@+id/fast_forward_rewind_mid_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ayp_ic_fast_forward_rewind_triangle_16dp"
android:visibility="invisible" />

<ImageView
android:id="@+id/fast_forward_rewind_right_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ayp_ic_fast_forward_rewind_triangle_16dp"
android:visibility="invisible" />

</LinearLayout>

<TextView
android:id="@+id/fast_forward_rewind_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"

android:padding="8dp"
android:textColor="@android:color/white"
android:textSize="12sp" />

</LinearLayout>
</RelativeLayout>
Loading

0 comments on commit 1321c8b

Please sign in to comment.