Skip to content

Commit 3cefd4d

Browse files
authored
feat: implement long press to play videos at 2x speed (#787)
Refs: #666
1 parent 767b801 commit 3cefd4d

File tree

6 files changed

+118
-2
lines changed

6 files changed

+118
-2
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- Long press gesture to play videos at 2x speed ([#666])
810

911
## [1.9.1] - 2025-11-25
1012
### Changed
@@ -247,6 +249,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
247249
[#754]: https://github.com/FossifyOrg/Gallery/issues/754
248250
[#759]: https://github.com/FossifyOrg/Gallery/issues/759
249251
[#786]: https://github.com/FossifyOrg/Gallery/issues/786
252+
[#666]: https://github.com/FossifyOrg/Gallery/issues/666
250253

251254
[Unreleased]: https://github.com/FossifyOrg/Gallery/compare/1.9.1...HEAD
252255
[1.9.1]: https://github.com/FossifyOrg/Gallery/compare/1.9.0...1.9.1

app/src/main/kotlin/org/fossify/gallery/fragments/VideoFragment.kt

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ import android.os.Bundle
1010
import android.os.Handler
1111
import android.util.DisplayMetrics
1212
import android.view.GestureDetector
13+
import android.view.HapticFeedbackConstants
1314
import android.view.LayoutInflater
1415
import android.view.MotionEvent
1516
import android.view.Surface
1617
import android.view.TextureView
1718
import android.view.View
19+
import android.view.ViewConfiguration
1820
import android.view.ViewGroup
1921
import android.view.WindowManager
2022
import android.widget.ImageView
23+
import android.widget.RelativeLayout
2124
import android.widget.SeekBar
2225
import android.widget.TextView
2326
import androidx.appcompat.content.res.AppCompatResources
@@ -66,6 +69,7 @@ import org.fossify.gallery.activities.BaseViewerActivity
6669
import org.fossify.gallery.activities.VideoActivity
6770
import org.fossify.gallery.databinding.PagerVideoItemBinding
6871
import org.fossify.gallery.extensions.config
72+
import org.fossify.gallery.extensions.getActionBarHeight
6973
import org.fossify.gallery.extensions.getBottomActionsHeight
7074
import org.fossify.gallery.extensions.getFormattedDuration
7175
import org.fossify.gallery.extensions.getFriendlyMessage
@@ -84,13 +88,17 @@ import org.fossify.gallery.views.MediaSideScroll
8488
import java.io.File
8589
import java.io.FileInputStream
8690
import java.text.DecimalFormat
91+
import kotlin.math.abs
8792

8893
@UnstableApi
8994
class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener,
9095
SeekBar.OnSeekBarChangeListener, PlaybackSpeedListener {
9196
companion object {
9297
private const val PROGRESS = "progress"
9398
private const val UPDATE_INTERVAL_MS = 250L
99+
private const val TOUCH_HOLD_DURATION_MS = 500L
100+
private const val TOUCH_HOLD_SPEED_MULTIPLIER = 2.0f
101+
private const val TOUCH_SLOP_DIVIDER = 3
94102
}
95103

96104
private var mIsFullscreen = false
@@ -118,6 +126,19 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener,
118126
private var mStoredBottomActions = true
119127
private var mStoredExtendedDetails = 0
120128
private var mStoredRememberLastVideoPosition = false
129+
private var mOriginalPlaybackSpeed = 1f
130+
private var mIsLongPressActive = false
131+
132+
private val mTouchHoldRunnable = Runnable {
133+
mView.parent.requestDisallowInterceptTouchEvent(true)
134+
// This code runs after the delay, only if the user is still holding down.
135+
mIsLongPressActive = true
136+
mOriginalPlaybackSpeed = mExoPlayer?.playbackParameters?.speed ?: mConfig.playbackSpeed
137+
mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
138+
updatePlaybackSpeed(TOUCH_HOLD_SPEED_MULTIPLIER)
139+
140+
mPlaybackSpeedPill.fadeIn()
141+
}
121142

122143
private lateinit var mTimeHolder: View
123144
private lateinit var mBrightnessSideScroll: MediaSideScroll
@@ -130,6 +151,10 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener,
130151
private lateinit var mCurrTimeView: TextView
131152
private lateinit var mPlayPauseButton: ImageView
132153
private lateinit var mSeekBar: SeekBar
154+
private lateinit var mPlaybackSpeedPill: TextView
155+
private var mTouchSlop = 0
156+
private var mInitialX = 0f
157+
private var mInitialY = 0f
133158

134159
override fun onCreateView(
135160
inflater: LayoutInflater,
@@ -142,6 +167,7 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener,
142167

143168
mMedium = arguments.getSerializable(MEDIUM) as Medium
144169
mConfig = context.config
170+
mTouchSlop = (ViewConfiguration.get(context).scaledTouchSlop) / TOUCH_SLOP_DIVIDER
145171
binding = PagerVideoItemBinding.inflate(inflater, container, false).apply {
146172
panoramaOutline.setOnClickListener { openPanorama() }
147173
bottomVideoTimeHolder.videoCurrTime.setOnClickListener { skip(false) }
@@ -170,6 +196,7 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener,
170196
}
171197

172198
mSeekBar = bottomVideoTimeHolder.videoSeekbar
199+
mPlaybackSpeedPill = playbackSpeedPill
173200
mSeekBar.setOnSeekBarChangeListener(this@VideoFragment)
174201
// adding an empty click listener just to avoid ripple animation at toggling fullscreen
175202
mSeekBar.setOnClickListener { }
@@ -178,6 +205,12 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener,
178205
mCurrTimeView = bottomVideoTimeHolder.videoCurrTime
179206
mBrightnessSideScroll = videoBrightnessController
180207
mVolumeSideScroll = videoVolumeController
208+
mBrightnessSideScroll.onVerticalScroll = {
209+
mTimerHandler.removeCallbacks(mTouchHoldRunnable)
210+
}
211+
mVolumeSideScroll.onVerticalScroll = {
212+
mTimerHandler.removeCallbacks(mTouchHoldRunnable)
213+
}
181214
mTextureView = videoSurface
182215
mTextureView.surfaceTextureListener = this@VideoFragment
183216

@@ -215,6 +248,10 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener,
215248
if (videoSurfaceFrame.controller.state.zoom == 1f) {
216249
handleEvent(event)
217250
}
251+
handleTouchHoldEvent(event)
252+
if (mIsLongPressActive) {
253+
return@setOnTouchListener true
254+
}
218255

219256
gestureDetector.onTouchEvent(event)
220257
false
@@ -223,6 +260,15 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener,
223260

224261
ViewCompat.setOnApplyWindowInsetsListener(binding.videoHolder) { _, insets ->
225262
val system = insets.getInsetsIgnoringVisibility(Type.systemBars())
263+
264+
val pillTopMargin = system.top + resources.getActionBarHeight(context) +
265+
resources.getDimension(org.fossify.commons.R.dimen.normal_margin).toInt()
266+
(mPlaybackSpeedPill.layoutParams as? RelativeLayout.LayoutParams)?.apply {
267+
setMargins(
268+
0, pillTopMargin, 0, 0
269+
)
270+
}
271+
226272
binding.bottomActionsDummy.updateLayoutParams<ViewGroup.LayoutParams> {
227273
height = resources.getBottomActionsHeight() + system.bottom
228274
}
@@ -288,7 +334,6 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener,
288334
doubleTap = { x, y ->
289335
doSkip(false)
290336
})
291-
292337
mVolumeSideScroll.initialize(
293338
activity,
294339
slideInfo,
@@ -942,4 +987,43 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener,
942987
mTextureView.layoutParams = this
943988
}
944989
}
990+
991+
private fun handleTouchHoldEvent(event: MotionEvent) {
992+
when (event.actionMasked) {
993+
MotionEvent.ACTION_DOWN -> {
994+
if (mIsPlaying && event.pointerCount == 1) {
995+
mInitialX = event.x
996+
mInitialY = event.y
997+
mTimerHandler.postDelayed(mTouchHoldRunnable, TOUCH_HOLD_DURATION_MS)
998+
}
999+
}
1000+
1001+
MotionEvent.ACTION_MOVE -> {
1002+
val deltaX = abs(event.x - mInitialX)
1003+
val deltaY = abs(event.y - mInitialY)
1004+
if (!mIsLongPressActive && (deltaX > mTouchSlop || deltaY > mTouchSlop)) {
1005+
mTimerHandler.removeCallbacks(mTouchHoldRunnable)
1006+
}
1007+
}
1008+
1009+
MotionEvent.ACTION_POINTER_DOWN -> {
1010+
if (!mIsLongPressActive) {
1011+
mTimerHandler.removeCallbacks(mTouchHoldRunnable)
1012+
}
1013+
}
1014+
1015+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
1016+
mTimerHandler.removeCallbacks(mTouchHoldRunnable)
1017+
stopHoldSpeedMultiplierGesture()
1018+
}
1019+
}
1020+
}
1021+
1022+
private fun stopHoldSpeedMultiplierGesture() {
1023+
if (mIsLongPressActive) {
1024+
updatePlaybackSpeed(mOriginalPlaybackSpeed)
1025+
mIsLongPressActive = false
1026+
mPlaybackSpeedPill.fadeOut()
1027+
}
1028+
}
9451029
}

app/src/main/kotlin/org/fossify/gallery/views/MediaSideScroll.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class MediaSideScroll(context: Context, attrs: AttributeSet) : RelativeLayout(co
3333

3434
private var mSlideInfoText = ""
3535
private var mSlideInfoFadeHandler = Handler()
36+
internal var onVerticalScroll: (() -> Unit)? = null
3637
private var mParentView: ViewGroup? = null
3738
private var activity: Activity? = null
3839
private var doubleTap: ((Float, Float) -> Unit)? = null
@@ -106,6 +107,7 @@ class MediaSideScroll(context: Context, attrs: AttributeSet) : RelativeLayout(co
106107
val diffY = mTouchDownY - event.rawY
107108

108109
if (Math.abs(diffY) > dragThreshold && Math.abs(diffY) > Math.abs(diffX)) {
110+
onVerticalScroll?.invoke()
109111
var percent = ((diffY / mViewHeight) * 100).toInt() * 3
110112
percent = Math.min(100, Math.max(-100, percent))
111113

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<shape xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:shape="rectangle">
4+
<solid android:color="#99000000" />
5+
<corners android:radius="@dimen/bottom_sheet_corner_radius" />
6+
7+
</shape>

app/src/main/res/layout/pager_video_item.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@
66
android:layout_height="match_parent"
77
android:layoutDirection="ltr">
88

9+
<TextView
10+
android:id="@+id/playback_speed_pill"
11+
android:layout_width="wrap_content"
12+
android:layout_height="wrap_content"
13+
android:layout_alignParentTop="true"
14+
android:layout_centerHorizontal="true"
15+
android:layout_marginTop="@dimen/label_start_margin"
16+
android:background="@drawable/playback_pill_background"
17+
android:elevation="10dp"
18+
android:paddingStart="@dimen/list_item_padding_horizontal"
19+
android:paddingTop="@dimen/list_item_padding_vertical"
20+
android:paddingEnd="@dimen/list_item_padding_horizontal"
21+
android:paddingBottom="@dimen/list_item_padding_vertical"
22+
android:text="@string/playback_speed_display_text"
23+
android:textColor="@android:color/white"
24+
android:textSize="@dimen/list_secondary_text_size"
25+
android:textStyle="bold"
26+
android:visibility="gone"
27+
tools:visibility="visible" />
28+
929
<ImageView
1030
android:id="@+id/video_preview"
1131
android:layout_width="match_parent"

app/src/main/res/values/donottranslate.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
<string name="raw">RAW</string>
66
<string name="svg">SVG</string>
77
<string name="package_name">org.fossify.gallery</string>
8-
8+
<string name="playback_speed_display_text">2x >></string>
99
</resources>

0 commit comments

Comments
 (0)