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