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 fa20ae1aa..f057171aa 100644
--- a/app/src/main/kotlin/org/fossify/gallery/activities/VideoPlayerActivity.kt
+++ b/app/src/main/kotlin/org/fossify/gallery/activities/VideoPlayerActivity.kt
@@ -1,5 +1,6 @@
package org.fossify.gallery.activities
+import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.ActivityInfo
@@ -30,10 +31,13 @@ import org.fossify.commons.extensions.*
import org.fossify.gallery.R
import org.fossify.gallery.databinding.ActivityVideoPlayerBinding
import org.fossify.gallery.extensions.*
+import org.fossify.gallery.fragments.PlaybackSpeedFragment
import org.fossify.gallery.helpers.*
+import org.fossify.gallery.interfaces.PlaybackSpeedListener
+import java.text.DecimalFormat
@UnstableApi
-open class VideoPlayerActivity : SimpleActivity(), SeekBar.OnSeekBarChangeListener, TextureView.SurfaceTextureListener {
+open class VideoPlayerActivity : SimpleActivity(), SeekBar.OnSeekBarChangeListener, TextureView.SurfaceTextureListener, PlaybackSpeedListener {
private val PLAY_WHEN_READY_DRAG_DELAY = 100L
private var mIsFullscreen = false
@@ -181,6 +185,7 @@ open class VideoPlayerActivity : SimpleActivity(), SeekBar.OnSeekBarChangeListen
binding.bottomVideoTimeHolder.videoCurrTime.setOnClickListener { doSkip(false) }
binding.bottomVideoTimeHolder.videoDuration.setOnClickListener { doSkip(true) }
binding.bottomVideoTimeHolder.videoTogglePlayPause.setOnClickListener { togglePlayPause() }
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed.setOnClickListener { showPlaybackSpeedPicker() }
binding.videoSurfaceFrame.setOnClickListener { toggleFullscreen() }
binding.videoSurfaceFrame.controller.settings.swallowDoubleTaps = true
@@ -256,6 +261,7 @@ open class VideoPlayerActivity : SimpleActivity(), SeekBar.OnSeekBarChangeListen
.setLoadControl(loadControl)
.build()
.apply {
+ setPlaybackSpeed(config.playbackSpeed)
setMediaSource(mediaSource)
setAudioAttributes(
AudioAttributes
@@ -299,10 +305,13 @@ open class VideoPlayerActivity : SimpleActivity(), SeekBar.OnSeekBarChangeListen
private fun videoPrepared() {
if (!mWasVideoStarted) {
binding.bottomVideoTimeHolder.videoTogglePlayPause.beVisible()
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed.beVisible()
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed.text = "${DecimalFormat("#.##").format(config.playbackSpeed)}x"
mDuration = (mExoPlayer!!.duration / 1000).toInt()
binding.bottomVideoTimeHolder.videoSeekbar.max = mDuration
binding.bottomVideoTimeHolder.videoDuration.text = mDuration.getFormattedDuration()
setPosition(mCurrTime)
+ updatePlaybackSpeed(config.playbackSpeed)
if (config.rememberLastVideoPosition) {
setLastVideoSavedPosition()
@@ -468,6 +477,7 @@ open class VideoPlayerActivity : SimpleActivity(), SeekBar.OnSeekBarChangeListen
binding.bottomVideoTimeHolder.videoPrevFile,
binding.bottomVideoTimeHolder.videoTogglePlayPause,
binding.bottomVideoTimeHolder.videoNextFile,
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed,
binding.bottomVideoTimeHolder.videoCurrTime,
binding.bottomVideoTimeHolder.videoSeekbar,
binding.bottomVideoTimeHolder.videoDuration,
@@ -480,6 +490,7 @@ open class VideoPlayerActivity : SimpleActivity(), SeekBar.OnSeekBarChangeListen
arrayOf(
binding.bottomVideoTimeHolder.videoPrevFile,
binding.bottomVideoTimeHolder.videoNextFile,
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed,
binding.bottomVideoTimeHolder.videoCurrTime,
binding.bottomVideoTimeHolder.videoDuration,
).forEach {
@@ -493,6 +504,27 @@ open class VideoPlayerActivity : SimpleActivity(), SeekBar.OnSeekBarChangeListen
}.start()
}
+ private fun showPlaybackSpeedPicker() {
+ val fragment = PlaybackSpeedFragment()
+ fragment.show(supportFragmentManager, PlaybackSpeedFragment::class.java.simpleName)
+ fragment.setListener(this)
+ }
+
+ override fun updatePlaybackSpeed(speed: Float) {
+ val isSlow = speed < 1f
+ if (isSlow != binding.bottomVideoTimeHolder.videoPlaybackSpeed.tag as? Boolean) {
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed.tag = isSlow
+
+ val drawableId = if (isSlow) R.drawable.ic_playback_speed_slow_vector else R.drawable.ic_playback_speed_vector
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed
+ .setCompoundDrawablesRelativeWithIntrinsicBounds(resources.getDrawable(drawableId), null, null, null)
+ }
+
+ @SuppressLint("SetTextI18n")
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed.text = "${DecimalFormat("#.##").format(speed)}x"
+ mExoPlayer?.setPlaybackSpeed(speed)
+ }
+
private fun initTimeHolder() {
var right = 0
var bottom = 0
diff --git a/app/src/main/kotlin/org/fossify/gallery/fragments/PlaybackSpeedFragment.kt b/app/src/main/kotlin/org/fossify/gallery/fragments/PlaybackSpeedFragment.kt
new file mode 100644
index 000000000..98805e744
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/gallery/fragments/PlaybackSpeedFragment.kt
@@ -0,0 +1,135 @@
+package org.fossify.gallery.fragments
+
+import android.graphics.drawable.LayerDrawable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.res.ResourcesCompat
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import org.fossify.commons.extensions.*
+import org.fossify.commons.views.MySeekBar
+import org.fossify.commons.views.MyTextView
+import org.fossify.gallery.R
+import org.fossify.gallery.databinding.FragmentPlaybackSpeedBinding
+import org.fossify.gallery.extensions.config
+import org.fossify.gallery.helpers.Config
+import org.fossify.gallery.interfaces.PlaybackSpeedListener
+
+class PlaybackSpeedFragment : BottomSheetDialogFragment() {
+ private val MIN_PLAYBACK_SPEED = 0.25f
+ private val MAX_PLAYBACK_SPEED = 3f
+ private val MAX_PROGRESS = (MAX_PLAYBACK_SPEED * 100 + MIN_PLAYBACK_SPEED * 100).toInt()
+ private val HALF_PROGRESS = MAX_PROGRESS / 2
+ private val STEP = 0.05f
+
+ private var seekBar: MySeekBar? = null
+ private var listener: PlaybackSpeedListener? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setStyle(STYLE_NORMAL, R.style.CustomBottomSheetDialogTheme)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ val config = requireContext().config
+ val binding = FragmentPlaybackSpeedBinding.inflate(inflater, container, false)
+ val background = ResourcesCompat.getDrawable(resources, org.fossify.commons.R.drawable.bottom_sheet_bg, requireContext().theme)
+ (background as LayerDrawable).findDrawableByLayerId(org.fossify.commons.R.id.bottom_sheet_background)
+ .applyColorFilter(requireContext().getProperBackgroundColor())
+
+ binding.apply {
+ seekBar = playbackSpeedSeekbar
+ root.setBackgroundDrawable(background)
+ requireContext().updateTextColors(playbackSpeedHolder)
+ playbackSpeedSlow.applyColorFilter(requireContext().getProperTextColor())
+ playbackSpeedFast.applyColorFilter(requireContext().getProperTextColor())
+ playbackSpeedSlow.setOnClickListener { reduceSpeed() }
+ playbackSpeedFast.setOnClickListener { increaseSpeed() }
+ initSeekbar(playbackSpeedSeekbar, playbackSpeedLabel, config)
+ }
+
+ return binding.root
+ }
+
+ private fun initSeekbar(seekbar: MySeekBar, speedLabel: MyTextView, config: Config) {
+ val formattedValue = formatPlaybackSpeed(config.playbackSpeed)
+ speedLabel.text = "${formattedValue}x"
+ seekbar.max = MAX_PROGRESS
+
+ val playbackSpeedProgress = config.playbackSpeedProgress
+ if (playbackSpeedProgress == -1) {
+ config.playbackSpeedProgress = HALF_PROGRESS
+ }
+ seekbar.progress = config.playbackSpeedProgress
+
+ var lastUpdatedProgress = config.playbackSpeedProgress
+ var lastUpdatedFormattedValue = formattedValue
+
+ seekbar.onSeekBarChangeListener { progress ->
+ val playbackSpeed = getPlaybackSpeed(progress)
+ if (playbackSpeed.toString() != lastUpdatedFormattedValue) {
+ lastUpdatedProgress = progress
+ lastUpdatedFormattedValue = playbackSpeed.toString()
+ config.playbackSpeed = playbackSpeed
+ config.playbackSpeedProgress = progress
+
+ speedLabel.text = "${formatPlaybackSpeed(playbackSpeed)}x"
+ listener?.updatePlaybackSpeed(playbackSpeed)
+ } else {
+ seekbar.progress = lastUpdatedProgress
+ }
+ }
+ }
+
+ private fun getPlaybackSpeed(progress: Int): Float {
+ var playbackSpeed = when {
+ progress < HALF_PROGRESS -> {
+ val lowerProgressPercent = progress / HALF_PROGRESS.toFloat()
+ val lowerProgress = (1 - MIN_PLAYBACK_SPEED) * lowerProgressPercent + MIN_PLAYBACK_SPEED
+ lowerProgress
+ }
+
+ progress > HALF_PROGRESS -> {
+ val upperProgressPercent = progress / HALF_PROGRESS.toFloat() - 1
+ val upperDiff = MAX_PLAYBACK_SPEED - 1
+ upperDiff * upperProgressPercent + 1
+ }
+
+ else -> 1f
+ }
+ playbackSpeed = Math.min(Math.max(playbackSpeed, MIN_PLAYBACK_SPEED), MAX_PLAYBACK_SPEED)
+ val stepMultiplier = 1 / STEP
+ return Math.round(playbackSpeed * stepMultiplier) / stepMultiplier
+ }
+
+ private fun reduceSpeed() {
+ var currentProgress = seekBar?.progress ?: return
+ val currentSpeed = requireContext().config.playbackSpeed
+ while (currentProgress > 0) {
+ val newSpeed = getPlaybackSpeed(--currentProgress)
+ if (newSpeed != currentSpeed) {
+ seekBar!!.progress = currentProgress
+ break
+ }
+ }
+ }
+
+ private fun increaseSpeed() {
+ var currentProgress = seekBar?.progress ?: return
+ val currentSpeed = requireContext().config.playbackSpeed
+ while (currentProgress < MAX_PROGRESS) {
+ val newSpeed = getPlaybackSpeed(++currentProgress)
+ if (newSpeed != currentSpeed) {
+ seekBar!!.progress = currentProgress
+ break
+ }
+ }
+ }
+
+ private fun formatPlaybackSpeed(value: Float) = String.format("%.2f", value)
+
+ fun setListener(playbackSpeedListener: PlaybackSpeedListener) {
+ listener = playbackSpeedListener
+ }
+}
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 66af31e86..ac29c731b 100644
--- a/app/src/main/kotlin/org/fossify/gallery/fragments/VideoFragment.kt
+++ b/app/src/main/kotlin/org/fossify/gallery/fragments/VideoFragment.kt
@@ -1,5 +1,6 @@
package org.fossify.gallery.fragments
+import android.annotation.SuppressLint
import android.content.res.Configuration
import android.graphics.Point
import android.graphics.SurfaceTexture
@@ -34,13 +35,15 @@ import org.fossify.gallery.extensions.config
import org.fossify.gallery.extensions.hasNavBar
import org.fossify.gallery.extensions.parseFileChannel
import org.fossify.gallery.helpers.*
+import org.fossify.gallery.interfaces.PlaybackSpeedListener
import org.fossify.gallery.models.Medium
import org.fossify.gallery.views.MediaSideScroll
import java.io.File
import java.io.FileInputStream
+import java.text.DecimalFormat
@UnstableApi
-class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, SeekBar.OnSeekBarChangeListener {
+class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, SeekBar.OnSeekBarChangeListener, PlaybackSpeedListener {
private val PROGRESS = "progress"
private var mIsFullscreen = false
@@ -94,6 +97,7 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, S
bottomVideoTimeHolder.videoDuration.setOnClickListener { skip(true) }
videoHolder.setOnClickListener { toggleFullscreen() }
videoPreview.setOnClickListener { toggleFullscreen() }
+ bottomVideoTimeHolder.videoPlaybackSpeed.setOnClickListener { showPlaybackSpeedPicker() }
videoSurfaceFrame.controller.settings.swallowDoubleTaps = true
videoPlayOutline.setOnClickListener {
@@ -390,6 +394,7 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, S
if (mConfig.loopVideos && listener?.isSlideShowActive() == false) {
repeatMode = Player.REPEAT_MODE_ONE
}
+ setPlaybackSpeed(mConfig.playbackSpeed)
setMediaSource(mediaSource)
setAudioAttributes(
AudioAttributes
@@ -521,7 +526,8 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, S
arrayOf(
binding.bottomVideoTimeHolder.videoCurrTime,
binding.bottomVideoTimeHolder.videoDuration,
- binding.bottomVideoTimeHolder.videoTogglePlayPause
+ binding.bottomVideoTimeHolder.videoTogglePlayPause,
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed
).forEach {
it.isClickable = !mIsFullscreen
}
@@ -538,6 +544,27 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, S
}
}
+ private fun showPlaybackSpeedPicker() {
+ val fragment = PlaybackSpeedFragment()
+ childFragmentManager.beginTransaction().add(fragment, fragment::class.java.simpleName).commit()
+ fragment.setListener(this)
+ }
+
+ override fun updatePlaybackSpeed(speed: Float) {
+ val isSlow = speed < 1f
+ if (isSlow != binding.bottomVideoTimeHolder.videoPlaybackSpeed.tag as? Boolean) {
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed.tag = isSlow
+
+ val drawableId = if (isSlow) R.drawable.ic_playback_speed_slow_vector else R.drawable.ic_playback_speed_vector
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed
+ .setCompoundDrawablesRelativeWithIntrinsicBounds(resources.getDrawable(drawableId), null, null, null)
+ }
+
+ @SuppressLint("SetTextI18n")
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed.text = "${DecimalFormat("#.##").format(speed)}x"
+ mExoPlayer?.setPlaybackSpeed(speed)
+ }
+
private fun getExtendedDetailsY(height: Int): Float {
val smallMargin = context?.resources?.getDimension(org.fossify.commons.R.dimen.small_margin) ?: return 0f
val fullscreenOffset = smallMargin + if (mIsFullscreen) 0 else requireContext().navigationBarHeight
@@ -662,6 +689,8 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, S
if (!mWasVideoStarted) {
binding.videoPlayOutline.beGone()
mPlayPauseButton.beVisible()
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed.beVisible()
+ binding.bottomVideoTimeHolder.videoPlaybackSpeed.text = "${DecimalFormat("#.##").format(mConfig.playbackSpeed)}x"
}
mWasVideoStarted = true
@@ -736,6 +765,7 @@ class VideoFragment : ViewPagerFragment(), TextureView.SurfaceTextureListener, S
mExoPlayer?.seekTo(mPositionAtPause)
mPositionAtPause = 0L
}
+ updatePlaybackSpeed(mConfig.playbackSpeed)
playVideo()
}
mWasPlayerInited = true
diff --git a/app/src/main/kotlin/org/fossify/gallery/helpers/Config.kt b/app/src/main/kotlin/org/fossify/gallery/helpers/Config.kt
index c721659a6..13ac503af 100644
--- a/app/src/main/kotlin/org/fossify/gallery/helpers/Config.kt
+++ b/app/src/main/kotlin/org/fossify/gallery/helpers/Config.kt
@@ -169,6 +169,14 @@ class Config(context: Context) : BaseConfig(context) {
get() = prefs.getBoolean(MAX_BRIGHTNESS, false)
set(maxBrightness) = prefs.edit().putBoolean(MAX_BRIGHTNESS, maxBrightness).apply()
+ var playbackSpeed: Float
+ get() = prefs.getFloat(PLAYBACK_SPEED, 1f)
+ set(playbackSpeed) = prefs.edit().putFloat(PLAYBACK_SPEED, playbackSpeed).apply()
+
+ var playbackSpeedProgress: Int
+ get() = prefs.getInt(PLAYBACK_SPEED_PROGRESS, -1)
+ set(playbackSpeedProgress) = prefs.edit().putInt(PLAYBACK_SPEED_PROGRESS, playbackSpeedProgress).apply()
+
var cropThumbnails: Boolean
get() = prefs.getBoolean(CROP_THUMBNAILS, true)
set(cropThumbnails) = prefs.edit().putBoolean(CROP_THUMBNAILS, cropThumbnails).apply()
diff --git a/app/src/main/kotlin/org/fossify/gallery/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/gallery/helpers/Constants.kt
index af11e1c04..f47f847c0 100644
--- a/app/src/main/kotlin/org/fossify/gallery/helpers/Constants.kt
+++ b/app/src/main/kotlin/org/fossify/gallery/helpers/Constants.kt
@@ -19,6 +19,8 @@ const val LOOP_VIDEOS = "loop_videos"
const val OPEN_VIDEOS_ON_SEPARATE_SCREEN = "open_videos_on_separate_screen"
const val ANIMATE_GIFS = "animate_gifs"
const val MAX_BRIGHTNESS = "max_brightness"
+const val PLAYBACK_SPEED = "playback_speed"
+const val PLAYBACK_SPEED_PROGRESS = "playback_speed_progress"
const val CROP_THUMBNAILS = "crop_thumbnails"
const val SHOW_THUMBNAIL_VIDEO_DURATION = "show_thumbnail_video_duration"
const val SCREEN_ROTATION = "screen_rotation"
diff --git a/app/src/main/kotlin/org/fossify/gallery/interfaces/PlaybackSpeedListener.kt b/app/src/main/kotlin/org/fossify/gallery/interfaces/PlaybackSpeedListener.kt
new file mode 100644
index 000000000..8da15bbc9
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/gallery/interfaces/PlaybackSpeedListener.kt
@@ -0,0 +1,5 @@
+package org.fossify.gallery.interfaces
+
+interface PlaybackSpeedListener {
+ fun updatePlaybackSpeed(speed: Float)
+}
diff --git a/app/src/main/res/drawable/ic_playback_speed_slow_vector.xml b/app/src/main/res/drawable/ic_playback_speed_slow_vector.xml
new file mode 100644
index 000000000..12eabf7dd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_playback_speed_slow_vector.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_playback_speed_vector.xml b/app/src/main/res/drawable/ic_playback_speed_vector.xml
new file mode 100644
index 000000000..4ae4c503e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_playback_speed_vector.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/layout/bottom_video_time_holder.xml b/app/src/main/res/layout/bottom_video_time_holder.xml
index f5bd1e8ff..c374a9370 100644
--- a/app/src/main/res/layout/bottom_video_time_holder.xml
+++ b/app/src/main/res/layout/bottom_video_time_holder.xml
@@ -1,54 +1,78 @@
-
+
+
+ android:visibility="invisible"
+ app:layout_constraintEnd_toStartOf="@id/video_toggle_play_pause"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:visibility="visible" />
+ android:visibility="invisible"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:visibility="visible" />
+ android:visibility="invisible"
+ app:layout_constraintStart_toEndOf="@id/video_toggle_play_pause"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:visibility="visible" />
+ android:paddingBottom="@dimen/activity_margin"
+ app:layout_constraintEnd_toStartOf="@+id/video_duration"
+ app:layout_constraintStart_toEndOf="@+id/video_curr_time"
+ app:layout_constraintTop_toBottomOf="@+id/video_toggle_play_pause" />
-
+
diff --git a/app/src/main/res/layout/fragment_playback_speed.xml b/app/src/main/res/layout/fragment_playback_speed.xml
new file mode 100644
index 000000000..d982a564a
--- /dev/null
+++ b/app/src/main/res/layout/fragment_playback_speed.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index e40795fee..d5e24083d 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -27,4 +27,5 @@
30dp
180dp
180dp
+ 36dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 302a55d00..542a0df14 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -182,6 +182,7 @@
Delete empty folders after deleting their content
Allow controlling photo brightness with vertical gestures
Allow controlling video volume and brightness with vertical gestures
+ Playback speed
Show folder media count on the main view
Show extended details over fullscreen media
Manage extended details
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index d6a419858..21d1b34df 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -2,6 +2,14 @@
+
+
+
+