diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ee22342..1dabf08fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ 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 ([#666]) ## [1.9.1] - 2025-11-25 ### Changed @@ -247,6 +249,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 +[#666]: https://github.com/FossifyOrg/Gallery/issues/666 [Unreleased]: https://github.com/FossifyOrg/Gallery/compare/1.9.1...HEAD [1.9.1]: https://github.com/FossifyOrg/Gallery/compare/1.9.0...1.9.1 diff --git a/app/src/main/kotlin/org/fossify/gallery/fragments/VideoFragment.kt b/app/src/main/kotlin/org/fossify/gallery/fragments/VideoFragment.kt index 3151fe012..c8d28df12 100644 --- a/app/src/main/kotlin/org/fossify/gallery/fragments/VideoFragment.kt +++ b/app/src/main/kotlin/org/fossify/gallery/fragments/VideoFragment.kt @@ -10,14 +10,17 @@ import android.os.Bundle import android.os.Handler import android.util.DisplayMetrics import android.view.GestureDetector +import android.view.HapticFeedbackConstants import android.view.LayoutInflater import android.view.MotionEvent import android.view.Surface import android.view.TextureView import android.view.View +import android.view.ViewConfiguration import android.view.ViewGroup import android.view.WindowManager import android.widget.ImageView +import android.widget.RelativeLayout import android.widget.SeekBar import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources @@ -66,6 +69,7 @@ import org.fossify.gallery.activities.BaseViewerActivity import org.fossify.gallery.activities.VideoActivity import org.fossify.gallery.databinding.PagerVideoItemBinding import org.fossify.gallery.extensions.config +import org.fossify.gallery.extensions.getActionBarHeight import org.fossify.gallery.extensions.getBottomActionsHeight import org.fossify.gallery.extensions.getFormattedDuration import org.fossify.gallery.extensions.getFriendlyMessage @@ -84,6 +88,7 @@ import org.fossify.gallery.views.MediaSideScroll import java.io.File import java.io.FileInputStream import java.text.DecimalFormat +import kotlin.math.abs @UnstableApi class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, @@ -91,6 +96,9 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, companion object { private const val PROGRESS = "progress" private const val UPDATE_INTERVAL_MS = 250L + private const val TOUCH_HOLD_DURATION_MS = 500L + private const val TOUCH_HOLD_SPEED_MULTIPLIER = 2.0f + private const val TOUCH_SLOP_DIVIDER = 3 } private var mIsFullscreen = false @@ -118,6 +126,19 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, private var mStoredBottomActions = true private var mStoredExtendedDetails = 0 private var mStoredRememberLastVideoPosition = false + private var mOriginalPlaybackSpeed = 1f + private var mIsLongPressActive = false + + private val mTouchHoldRunnable = Runnable { + mView.parent.requestDisallowInterceptTouchEvent(true) + // This code runs after the delay, only if the user is still holding down. + mIsLongPressActive = true + mOriginalPlaybackSpeed = mExoPlayer?.playbackParameters?.speed ?: mConfig.playbackSpeed + mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + updatePlaybackSpeed(TOUCH_HOLD_SPEED_MULTIPLIER) + + mPlaybackSpeedPill.fadeIn() + } private lateinit var mTimeHolder: View private lateinit var mBrightnessSideScroll: MediaSideScroll @@ -130,6 +151,10 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, private lateinit var mCurrTimeView: TextView private lateinit var mPlayPauseButton: ImageView private lateinit var mSeekBar: SeekBar + private lateinit var mPlaybackSpeedPill: TextView + private var mTouchSlop = 0 + private var mInitialX = 0f + private var mInitialY = 0f override fun onCreateView( inflater: LayoutInflater, @@ -142,6 +167,7 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, mMedium = arguments.getSerializable(MEDIUM) as Medium mConfig = context.config + mTouchSlop = (ViewConfiguration.get(context).scaledTouchSlop) / TOUCH_SLOP_DIVIDER binding = PagerVideoItemBinding.inflate(inflater, container, false).apply { panoramaOutline.setOnClickListener { openPanorama() } bottomVideoTimeHolder.videoCurrTime.setOnClickListener { skip(false) } @@ -170,6 +196,7 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, } mSeekBar = bottomVideoTimeHolder.videoSeekbar + mPlaybackSpeedPill = playbackSpeedPill mSeekBar.setOnSeekBarChangeListener(this@VideoFragment) // adding an empty click listener just to avoid ripple animation at toggling fullscreen mSeekBar.setOnClickListener { } @@ -178,6 +205,12 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, mCurrTimeView = bottomVideoTimeHolder.videoCurrTime mBrightnessSideScroll = videoBrightnessController mVolumeSideScroll = videoVolumeController + mBrightnessSideScroll.onVerticalScroll = { + mTimerHandler.removeCallbacks(mTouchHoldRunnable) + } + mVolumeSideScroll.onVerticalScroll = { + mTimerHandler.removeCallbacks(mTouchHoldRunnable) + } mTextureView = videoSurface mTextureView.surfaceTextureListener = this@VideoFragment @@ -215,6 +248,10 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, if (videoSurfaceFrame.controller.state.zoom == 1f) { handleEvent(event) } + handleTouchHoldEvent(event) + if (mIsLongPressActive) { + return@setOnTouchListener true + } gestureDetector.onTouchEvent(event) false @@ -223,6 +260,15 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, ViewCompat.setOnApplyWindowInsetsListener(binding.videoHolder) { _, insets -> val system = insets.getInsetsIgnoringVisibility(Type.systemBars()) + + val pillTopMargin = system.top + resources.getActionBarHeight(context) + + resources.getDimension(org.fossify.commons.R.dimen.normal_margin).toInt() + (mPlaybackSpeedPill.layoutParams as? RelativeLayout.LayoutParams)?.apply { + setMargins( + 0, pillTopMargin, 0, 0 + ) + } + binding.bottomActionsDummy.updateLayoutParams { height = resources.getBottomActionsHeight() + system.bottom } @@ -288,7 +334,6 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, doubleTap = { x, y -> doSkip(false) }) - mVolumeSideScroll.initialize( activity, slideInfo, @@ -942,4 +987,43 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, mTextureView.layoutParams = this } } + + private fun handleTouchHoldEvent(event: MotionEvent) { + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + if (mIsPlaying && event.pointerCount == 1) { + mInitialX = event.x + mInitialY = event.y + mTimerHandler.postDelayed(mTouchHoldRunnable, TOUCH_HOLD_DURATION_MS) + } + } + + MotionEvent.ACTION_MOVE -> { + val deltaX = abs(event.x - mInitialX) + val deltaY = abs(event.y - mInitialY) + if (!mIsLongPressActive && (deltaX > mTouchSlop || deltaY > mTouchSlop)) { + mTimerHandler.removeCallbacks(mTouchHoldRunnable) + } + } + + MotionEvent.ACTION_POINTER_DOWN -> { + if (!mIsLongPressActive) { + mTimerHandler.removeCallbacks(mTouchHoldRunnable) + } + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + mTimerHandler.removeCallbacks(mTouchHoldRunnable) + stopHoldSpeedMultiplierGesture() + } + } + } + + private fun stopHoldSpeedMultiplierGesture() { + if (mIsLongPressActive) { + updatePlaybackSpeed(mOriginalPlaybackSpeed) + mIsLongPressActive = false + mPlaybackSpeedPill.fadeOut() + } + } } diff --git a/app/src/main/kotlin/org/fossify/gallery/views/MediaSideScroll.kt b/app/src/main/kotlin/org/fossify/gallery/views/MediaSideScroll.kt index f5536eef8..e9497d6fc 100644 --- a/app/src/main/kotlin/org/fossify/gallery/views/MediaSideScroll.kt +++ b/app/src/main/kotlin/org/fossify/gallery/views/MediaSideScroll.kt @@ -33,6 +33,7 @@ class MediaSideScroll(context: Context, attrs: AttributeSet) : RelativeLayout(co private var mSlideInfoText = "" private var mSlideInfoFadeHandler = Handler() + internal var onVerticalScroll: (() -> Unit)? = null private var mParentView: ViewGroup? = null private var activity: Activity? = null private var doubleTap: ((Float, Float) -> Unit)? = null @@ -106,6 +107,7 @@ class MediaSideScroll(context: Context, attrs: AttributeSet) : RelativeLayout(co val diffY = mTouchDownY - event.rawY if (Math.abs(diffY) > dragThreshold && Math.abs(diffY) > Math.abs(diffX)) { + onVerticalScroll?.invoke() var percent = ((diffY / mViewHeight) * 100).toInt() * 3 percent = Math.min(100, Math.max(-100, percent)) diff --git a/app/src/main/res/drawable/playback_pill_background.xml b/app/src/main/res/drawable/playback_pill_background.xml new file mode 100644 index 000000000..f3084f80b --- /dev/null +++ b/app/src/main/res/drawable/playback_pill_background.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/layout/pager_video_item.xml b/app/src/main/res/layout/pager_video_item.xml index 4d3d875be..5a4651800 100644 --- a/app/src/main/res/layout/pager_video_item.xml +++ b/app/src/main/res/layout/pager_video_item.xml @@ -6,6 +6,26 @@ android:layout_height="match_parent" android:layoutDirection="ltr"> + + RAW SVG org.fossify.gallery - + 2x >>