Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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),
)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}

}
20 changes: 20 additions & 0 deletions app/src/main/res/layout/activity_video_player.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/playback_speed_pill"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/label_start_margin"
android:background="@drawable/playback_pill_background"
android:elevation="10dp"
android:paddingStart="@dimen/list_item_padding_horizontal"
android:paddingTop="@dimen/list_item_padding_vertical"
android:paddingEnd="@dimen/list_item_padding_horizontal"
android:paddingBottom="@dimen/list_item_padding_vertical"
android:text="@string/playback_speed_display_text"
android:textColor="@android:color/white"
android:textSize="@dimen/list_secondary_text_size"
android:textStyle="bold"
android:visibility="gone"
tools:visibility="visible" />

<com.alexvasilkov.gestures.GestureFrameLayout
android:id="@+id/video_surface_frame"
android:layout_width="match_parent"
Expand Down
Loading