diff --git a/CHANGELOG.md b/CHANGELOG.md
index f570ae447..3bbd911e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Added
+- Long press gesture to play videos at 2x speed in separate video player ([#830])
+
### Fixed
- Fixed invisible color picker button in black themes ([#337])
- Fixed issue with separate video player not respecting paused state when seeking ([#831])
@@ -265,6 +268,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#754]: https://github.com/FossifyOrg/Gallery/issues/754
[#759]: https://github.com/FossifyOrg/Gallery/issues/759
[#786]: https://github.com/FossifyOrg/Gallery/issues/786
+[#830]: https://github.com/FossifyOrg/Gallery/issues/830
[#831]: https://github.com/FossifyOrg/Gallery/issues/831
[#800]: https://github.com/FossifyOrg/Gallery/issues/800
diff --git a/app/src/main/kotlin/org/fossify/gallery/activities/VideoPlayerActivity.kt b/app/src/main/kotlin/org/fossify/gallery/activities/VideoPlayerActivity.kt
index 15a40adf9..a4868262c 100644
--- a/app/src/main/kotlin/org/fossify/gallery/activities/VideoPlayerActivity.kt
+++ b/app/src/main/kotlin/org/fossify/gallery/activities/VideoPlayerActivity.kt
@@ -17,13 +17,19 @@ import android.os.Bundle
import android.os.Handler
import android.util.DisplayMetrics
import android.view.GestureDetector
+import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.Surface
import android.view.TextureView
import android.view.View
+import android.view.ViewConfiguration
import android.view.WindowManager
+import android.widget.RelativeLayout
import android.widget.SeekBar
+import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
@@ -45,6 +51,7 @@ import org.fossify.commons.extensions.beGone
import org.fossify.commons.extensions.beVisible
import org.fossify.commons.extensions.beVisibleIf
import org.fossify.commons.extensions.fadeIn
+import org.fossify.commons.extensions.fadeOut
import org.fossify.commons.extensions.getColoredDrawableWithColor
import org.fossify.commons.extensions.getFilenameFromUri
import org.fossify.commons.extensions.getFormattedDuration
@@ -57,14 +64,13 @@ import org.fossify.commons.extensions.viewBinding
import org.fossify.gallery.R
import org.fossify.gallery.databinding.ActivityVideoPlayerBinding
import org.fossify.gallery.extensions.config
+import org.fossify.gallery.extensions.getActionBarHeight
import org.fossify.gallery.extensions.getFormattedDuration
import org.fossify.gallery.extensions.getFriendlyMessage
import org.fossify.gallery.extensions.hideSystemUI
-import org.fossify.gallery.extensions.mute
import org.fossify.gallery.extensions.openPath
import org.fossify.gallery.extensions.shareMediumPath
import org.fossify.gallery.extensions.showSystemUI
-import org.fossify.gallery.extensions.unmute
import org.fossify.gallery.fragments.PlaybackSpeedFragment
import org.fossify.gallery.helpers.DRAG_THRESHOLD
import org.fossify.gallery.helpers.EXOPLAYER_MAX_BUFFER_MS
@@ -79,6 +85,8 @@ import org.fossify.gallery.helpers.ROTATE_BY_DEVICE_ROTATION
import org.fossify.gallery.helpers.ROTATE_BY_SYSTEM_SETTING
import org.fossify.gallery.helpers.SHOW_NEXT_ITEM
import org.fossify.gallery.helpers.SHOW_PREV_ITEM
+import org.fossify.gallery.helpers.VideoGestureCallbacks
+import org.fossify.gallery.helpers.VideoGestureHelper
import org.fossify.gallery.interfaces.PlaybackSpeedListener
import java.text.DecimalFormat
import kotlin.math.max
@@ -114,6 +122,9 @@ open class VideoPlayerActivity : BaseViewerActivity(), SeekBar.OnSeekBarChangeLi
private var mPlayWhenReadyHandler = Handler()
private var mIgnoreCloseDown = false
+ private var mTouchSlop = 0
+ private lateinit var mPlaybackSpeedPill: TextView
+ private lateinit var videoGestureHelper: VideoGestureHelper
private val binding by viewBinding(ActivityVideoPlayerBinding::inflate)
@@ -126,6 +137,22 @@ open class VideoPlayerActivity : BaseViewerActivity(), SeekBar.OnSeekBarChangeLi
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
+ mPlaybackSpeedPill = binding.playbackSpeedPill
+ mTouchSlop = (ViewConfiguration.get(this).scaledTouchSlop)
+ videoGestureHelper = VideoGestureHelper(
+ touchSlop = mTouchSlop,
+ callbacks = VideoGestureCallbacks(
+ isPlaying = { mIsPlaying },
+ getCurrentSpeed = { config.playbackSpeed },
+ setPlaybackSpeed = { speed ->
+ mExoPlayer?.setPlaybackSpeed(speed) // Set to 2x speed
+ updatePlaybackSpeed(speed)
+ },
+ showPill = { mPlaybackSpeedPill.fadeIn() },
+ hidePill = { mPlaybackSpeedPill.fadeOut() },
+ performHaptic = { contentHolder.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) },
+ disallowParentIntercept = { contentHolder.parent.requestDisallowInterceptTouchEvent(true) })
+ )
setupEdgeToEdge(
padBottomSystem = listOf(binding.bottomVideoTimeHolder.root),
)
@@ -226,6 +253,21 @@ open class VideoPlayerActivity : BaseViewerActivity(), SeekBar.OnSeekBarChangeLi
private fun initPlayer() {
mUri = intent.data ?: return
binding.videoToolbar.title = getFilenameFromUri(mUri!!)
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+
+ // Calculate the top margin using the safe inset value
+ val pillTopMargin = systemBars.top + resources.getActionBarHeight(this) +
+ resources.getDimension(org.fossify.commons.R.dimen.normal_margin).toInt()
+
+ // Apply the margin to the pill
+ (mPlaybackSpeedPill.layoutParams as? RelativeLayout.LayoutParams)?.apply {
+ setMargins(0, pillTopMargin, 0, 0)
+ }
+
+ // Return the insets so other views can consume them if needed
+ insets
+ }
initTimeHolder()
showSystemUI()
@@ -271,6 +313,12 @@ open class VideoPlayerActivity : BaseViewerActivity(), SeekBar.OnSeekBarChangeLi
})
binding.videoSurfaceFrame.setOnTouchListener { view, event ->
+ videoGestureHelper.onTouchEvent(event)
+
+ if (videoGestureHelper.isLongPressActive) {
+ return@setOnTouchListener true
+ }
+
handleEvent(event)
gestureDetector.onTouchEvent(event)
false
@@ -581,7 +629,11 @@ open class VideoPlayerActivity : BaseViewerActivity(), SeekBar.OnSeekBarChangeLi
}
private fun toggleFullscreen() {
- fullscreenToggled(!mIsFullscreen)
+ if (!videoGestureHelper.wasLongPressHandled()) {
+ fullscreenToggled(!mIsFullscreen)
+ } else {
+ videoGestureHelper.updateLongPressHandled()
+ }
}
private fun fullscreenToggled(isFullScreen: Boolean) {
@@ -699,7 +751,9 @@ open class VideoPlayerActivity : BaseViewerActivity(), SeekBar.OnSeekBarChangeLi
mProgressAtDown = mExoPlayer!!.currentPosition
}
- MotionEvent.ACTION_POINTER_DOWN -> mIgnoreCloseDown = true
+ MotionEvent.ACTION_POINTER_DOWN -> {
+ mIgnoreCloseDown = true
+ }
MotionEvent.ACTION_MOVE -> {
val diffX = event.rawX - mTouchDownX
val diffY = event.rawY - mTouchDownY
diff --git a/app/src/main/kotlin/org/fossify/gallery/helpers/VideoGestureHelper.kt b/app/src/main/kotlin/org/fossify/gallery/helpers/VideoGestureHelper.kt
new file mode 100644
index 000000000..afdc43f34
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/gallery/helpers/VideoGestureHelper.kt
@@ -0,0 +1,93 @@
+package org.fossify.gallery.helpers
+
+import android.os.Handler
+import android.os.Looper
+import android.view.MotionEvent
+import kotlin.math.abs
+
+data class VideoGestureCallbacks(
+ val isPlaying: () -> Boolean,
+ val getCurrentSpeed: () -> Float,
+ val setPlaybackSpeed: (Float) -> Unit,
+ val showPill: () -> Unit,
+ val hidePill: () -> Unit,
+ val performHaptic: () -> Unit,
+ val disallowParentIntercept: () -> Unit
+)
+
+class VideoGestureHelper(
+ private val touchSlop: Int,
+ private val callbacks: VideoGestureCallbacks
+) {
+ companion object {
+ private const val TOUCH_HOLD_DURATION_MS = 500L
+ private const val TOUCH_HOLD_SPEED_MULTIPLIER = 2.0f
+ }
+
+ private val handler = Handler(Looper.getMainLooper())
+
+ private var initialX = 0f
+ private var initialY = 0f
+ private var originalSpeed = 1f
+ internal var isLongPressActive = false
+ private var wasLongPressHandled = false
+
+ private val touchHoldRunnable = Runnable {
+ if (callbacks.isPlaying()) {
+ callbacks.disallowParentIntercept()
+ isLongPressActive = true
+ originalSpeed = callbacks.getCurrentSpeed()
+ callbacks.performHaptic()
+ callbacks.setPlaybackSpeed(TOUCH_HOLD_SPEED_MULTIPLIER)
+ callbacks.showPill()
+ }
+ }
+
+ fun onTouchEvent(event: MotionEvent) {
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ if (callbacks.isPlaying() && event.pointerCount == 1) {
+ initialX = event.x
+ initialY = event.y
+ handler.postDelayed(touchHoldRunnable, TOUCH_HOLD_DURATION_MS)
+ }
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ val dx = abs(event.x - initialX)
+ val dy = abs(event.y - initialY)
+ if (!isLongPressActive && (dx > touchSlop || dy > touchSlop)) {
+ handler.removeCallbacks(touchHoldRunnable)
+ }
+ }
+
+ MotionEvent.ACTION_POINTER_DOWN -> {
+ if (!isLongPressActive) {
+ handler.removeCallbacks(touchHoldRunnable)
+ }
+ }
+
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_CANCEL -> {
+ handler.removeCallbacks(touchHoldRunnable)
+ stop()
+ }
+ }
+ }
+
+ fun wasLongPressHandled() = wasLongPressHandled
+
+ fun updateLongPressHandled() {
+ wasLongPressHandled = false
+ }
+
+ fun stop() {
+ if (isLongPressActive) {
+ wasLongPressHandled = true
+ callbacks.setPlaybackSpeed(originalSpeed)
+ isLongPressActive = false
+ callbacks.hidePill()
+ }
+ }
+
+}
diff --git a/app/src/main/res/layout/activity_video_player.xml b/app/src/main/res/layout/activity_video_player.xml
index db3e040ee..7410f40e1 100644
--- a/app/src/main/res/layout/activity_video_player.xml
+++ b/app/src/main/res/layout/activity_video_player.xml
@@ -6,6 +6,26 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+
+