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"> + +