Merge pull request #2040 from Kruna1Pate1/feat/brightness-volume-swipe-control

Add support for swipe gesture
This commit is contained in:
Bnyro 2022-11-25 16:12:33 +01:00 committed by GitHub
commit 55fbd3437a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 616 additions and 88 deletions

View File

@ -83,6 +83,7 @@ object PreferenceKeys {
const val ALTERNATIVE_PLAYER_LAYOUT = "alternative_player_layout" const val ALTERNATIVE_PLAYER_LAYOUT = "alternative_player_layout"
const val USE_HLS_OVER_DASH = "use_hls" const val USE_HLS_OVER_DASH = "use_hls"
const val QUEUE_AUTO_INSERT_RELATED = "queue_insert_related_videos" const val QUEUE_AUTO_INSERT_RELATED = "queue_insert_related_videos"
const val PLAYER_SWIPE_CONTROLS = "player_swipe_controls"
/** /**
* Background mode * Background mode

View File

@ -0,0 +1,15 @@
package com.github.libretube.extensions
fun Int.normalize(oldMin: Int, oldMax: Int, newMin: Int, newMax: Int): Int {
val oldRange = oldMax - oldMin
val newRange = newMax - newMin
return (this - oldMin) * newRange / oldRange + newMin
}
fun Float.normalize(oldMin: Float, oldMax: Float, newMin: Float, newMax: Float): Float {
val oldRange = oldMax - oldMin
val newRange = newMax - newMin
return (this - oldMin) * newRange / oldRange + newMin
}

View File

@ -70,6 +70,7 @@ class OfflinePlayerActivity : BaseActivity() {
binding.player.initialize( binding.player.initialize(
null, null,
binding.doubleTapOverlay.binding, binding.doubleTapOverlay.binding,
binding.playerGestureControlsView.binding,
null null
) )
} }

View File

@ -49,6 +49,7 @@ import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.DoubleTapOverlayBinding import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.databinding.FragmentPlayerBinding import com.github.libretube.databinding.FragmentPlayerBinding
import com.github.libretube.databinding.PlayerGestureControlsViewBinding
import com.github.libretube.db.DatabaseHelper import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder.Companion.Database import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.WatchPosition import com.github.libretube.db.obj.WatchPosition
@ -118,6 +119,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
lateinit var binding: FragmentPlayerBinding lateinit var binding: FragmentPlayerBinding
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
private lateinit var doubleTapOverlayBinding: DoubleTapOverlayBinding private lateinit var doubleTapOverlayBinding: DoubleTapOverlayBinding
private lateinit var playerGestureControlsViewBinding: PlayerGestureControlsViewBinding
private val viewModel: PlayerViewModel by activityViewModels() private val viewModel: PlayerViewModel by activityViewModels()
/** /**
@ -183,6 +185,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
exoPlayerView = binding.player exoPlayerView = binding.player
playerBinding = binding.player.binding playerBinding = binding.player.binding
doubleTapOverlayBinding = binding.doubleTapOverlay.binding doubleTapOverlayBinding = binding.doubleTapOverlay.binding
playerGestureControlsViewBinding = binding.playerGestureControlsView.binding
// Inflate the layout for this fragment // Inflate the layout for this fragment
return binding.root return binding.root
@ -775,6 +778,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
binding.player.initialize( binding.player.initialize(
this, this,
doubleTapOverlayBinding, doubleTapOverlayBinding,
playerGestureControlsViewBinding,
trackSelector trackSelector
) )

View File

@ -1,49 +0,0 @@
package com.github.libretube.ui.interfaces
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.view.View
abstract class DoubleTapListener : View.OnClickListener {
private val handler = Handler(Looper.getMainLooper())
private var lastClick = 0L
private var lastDoubleClick = 0L
abstract fun onDoubleClick()
abstract fun onSingleClick()
override fun onClick(v: View?) {
if (isSecondClick()) {
handler.removeCallbacks(runnable)
lastDoubleClick = elapsedTime()
onDoubleClick()
} else {
if (recentDoubleClick()) return
handler.removeCallbacks(runnable)
handler.postDelayed(runnable, MAX_TIME_DIFF)
lastClick = elapsedTime()
}
}
private val runnable = Runnable {
if (isSecondClick()) return@Runnable
onSingleClick()
}
private fun isSecondClick(): Boolean {
return elapsedTime() - lastClick < MAX_TIME_DIFF
}
private fun recentDoubleClick(): Boolean {
return elapsedTime() - lastDoubleClick < MAX_TIME_DIFF / 2
}
fun elapsedTime() = SystemClock.elapsedRealtime()
companion object {
private const val MAX_TIME_DIFF = 400L
}
}

View File

@ -0,0 +1,18 @@
package com.github.libretube.ui.interfaces
interface PlayerGestureOptions {
fun onSingleTap()
fun onDoubleTapCenterScreen()
fun onDoubleTapLeftScreen()
fun onDoubleTapRightScreen()
fun onSwipeLeftScreen(distanceY: Float)
fun onSwipeRightScreen(distanceY: Float)
fun onSwipeEnd()
}

View File

@ -1,6 +1,7 @@
package com.github.libretube.ui.views package com.github.libretube.ui.views
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Handler import android.os.Handler
@ -8,18 +9,24 @@ import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.WindowManager
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.databinding.DoubleTapOverlayBinding import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.databinding.PlayerGestureControlsViewBinding
import com.github.libretube.extensions.normalize
import com.github.libretube.extensions.toDp import com.github.libretube.extensions.toDp
import com.github.libretube.obj.BottomSheetItem import com.github.libretube.obj.BottomSheetItem
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.base.BaseActivity import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.interfaces.DoubleTapListener
import com.github.libretube.ui.interfaces.OnlinePlayerOptions import com.github.libretube.ui.interfaces.OnlinePlayerOptions
import com.github.libretube.ui.interfaces.PlayerGestureOptions
import com.github.libretube.ui.interfaces.PlayerOptions import com.github.libretube.ui.interfaces.PlayerOptions
import com.github.libretube.ui.sheets.BaseBottomSheet import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.PlaybackSpeedSheet import com.github.libretube.ui.sheets.PlaybackSpeedSheet
import com.github.libretube.util.AudioHelper
import com.github.libretube.util.BrightnessHelper
import com.github.libretube.util.PlayerGestureController
import com.github.libretube.util.PlayerHelper import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
import com.google.android.exoplayer2.PlaybackParameters import com.google.android.exoplayer2.PlaybackParameters
@ -33,8 +40,16 @@ import com.google.android.exoplayer2.util.RepeatModeUtil
internal class CustomExoPlayerView( internal class CustomExoPlayerView(
context: Context, context: Context,
attributeSet: AttributeSet? = null attributeSet: AttributeSet? = null
) : StyledPlayerView(context, attributeSet), PlayerOptions { ) : StyledPlayerView(context, attributeSet), PlayerOptions, PlayerGestureOptions {
val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this) val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this)
/**
* Objects for player tap and swipe gesture
*/
private lateinit var gestureViewBinding: PlayerGestureControlsViewBinding
private lateinit var playerGestureController: PlayerGestureController
private lateinit var brightnessHelper: BrightnessHelper
private lateinit var audioHelper: AudioHelper
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
/** /**
@ -45,16 +60,12 @@ internal class CustomExoPlayerView(
private val runnableHandler = Handler(Looper.getMainLooper()) private val runnableHandler = Handler(Looper.getMainLooper())
// the x-position of where the user clicked
private var xPos = 0F
var isPlayerLocked: Boolean = false var isPlayerLocked: Boolean = false
/** /**
* Preferences * Preferences
*/ */
var autoplayEnabled = PlayerHelper.autoPlayEnabled var autoplayEnabled = PlayerHelper.autoPlayEnabled
private var doubleTapAllowed = true
private var resizeModePref = PlayerHelper.resizeModePref private var resizeModePref = PlayerHelper.resizeModePref
@ -65,42 +76,23 @@ internal class CustomExoPlayerView(
if (isControllerFullyVisible) hideController() else showController() if (isControllerFullyVisible) hideController() else showController()
} }
private val doubleTouchListener = object : DoubleTapListener() {
override fun onDoubleClick() {
if (!doubleTapAllowed) return
val eventPositionPercentageX = xPos / width
when {
eventPositionPercentageX < 0.4 -> rewind()
eventPositionPercentageX > 0.6 -> forward()
else -> {
player?.let { player ->
if (player.isPlaying) {
player.pause()
} else {
player.play()
}
}
}
}
}
override fun onSingleClick() {
toggleController()
}
}
fun initialize( fun initialize(
playerViewInterface: OnlinePlayerOptions?, playerViewInterface: OnlinePlayerOptions?,
doubleTapOverlayBinding: DoubleTapOverlayBinding, doubleTapOverlayBinding: DoubleTapOverlayBinding,
playerGestureControlsViewBinding: PlayerGestureControlsViewBinding,
trackSelector: TrackSelector? trackSelector: TrackSelector?
) { ) {
this.playerOptionsInterface = playerViewInterface this.playerOptionsInterface = playerViewInterface
this.doubleTapOverlayBinding = doubleTapOverlayBinding this.doubleTapOverlayBinding = doubleTapOverlayBinding
this.trackSelector = trackSelector this.trackSelector = trackSelector
this.gestureViewBinding = playerGestureControlsViewBinding
this.playerGestureController = PlayerGestureController(context, this)
this.brightnessHelper = BrightnessHelper(context as Activity)
this.audioHelper = AudioHelper(context)
// set the double click listener for rewind/forward // Set touch listner for tap and swipe gestures.
setOnClickListener(doubleTouchListener) setOnTouchListener(playerGestureController)
initializeGestureProgress()
enableDoubleTapToSeek() enableDoubleTapToSeek()
initializeAdvancedOptions(context) initializeAdvancedOptions(context)
@ -144,10 +136,6 @@ internal class CustomExoPlayerView(
} }
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
// save the x position of the touch event
xPos = event.x
// listen for a double touch
doubleTouchListener.onClick(this)
return false return false
} }
@ -261,8 +249,8 @@ internal class CustomExoPlayerView(
binding.exoBottomBar.visibility = visibility binding.exoBottomBar.visibility = visibility
binding.closeImageButton.visibility = visibility binding.closeImageButton.visibility = visibility
// disable double tap to seek if the player is locked // disable tap and swipe gesture if the player is locked
doubleTapAllowed = !isLocked playerGestureController.isEnabled = isLocked
} }
private fun enableDoubleTapToSeek() { private fun enableDoubleTapToSeek() {
@ -331,6 +319,56 @@ internal class CustomExoPlayerView(
} }
} }
private fun initializeGestureProgress() {
val brightnessBar = gestureViewBinding.brightnessProgressBar
val volumeBar = gestureViewBinding.volumeProgressBar
brightnessBar.progress = if (brightnessHelper.brightness == WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) {
25.normalize(0, 100, 0, volumeBar.max)
} else {
brightnessHelper.getBrightnessWithScale(brightnessBar.max.toFloat()).toInt()
}
volumeBar.progress = audioHelper.getVolumeWithScale(volumeBar.max)
}
private fun updateBrightness(distance: Float) {
gestureViewBinding.brightnessControlView.visibility = View.VISIBLE
val bar = gestureViewBinding.brightnessProgressBar
if (bar.progress == 0) {
// If brightness progress goes to below 0, set to system brightness
if (distance <= 0) {
brightnessHelper.resetToSystemBrightness()
gestureViewBinding.brightnessImageView.setImageResource(R.drawable.ic_brightness_auto)
gestureViewBinding.brightnessTextView.text = resources.getString(R.string.auto)
return
}
gestureViewBinding.brightnessImageView.setImageResource(R.drawable.ic_brightness)
}
bar.incrementProgressBy(distance.toInt())
gestureViewBinding.brightnessTextView.text = "${bar.progress.normalize(0, bar.max, 0, 100)}"
brightnessHelper.setBrightnessWithScale(bar.progress.toFloat(), bar.max.toFloat())
}
private fun updateVolume(distance: Float) {
gestureViewBinding.volumeControlView.visibility = View.VISIBLE
val bar = gestureViewBinding.volumeProgressBar
if (bar.progress == 0) {
gestureViewBinding.volumeImageView.setImageResource(
when {
distance > 0 -> R.drawable.ic_volume_up
else -> R.drawable.ic_volume_off
}
)
}
bar.incrementProgressBy(distance.toInt())
audioHelper.setVolumeWithScale(bar.progress, bar.max)
gestureViewBinding.volumeTextView.text = "${bar.progress.normalize(0, bar.max, 0, 100)}"
}
override fun onAutoplayClicked() { override fun onAutoplayClicked() {
// autoplay options dialog // autoplay options dialog
BaseBottomSheet() BaseBottomSheet()
@ -408,4 +446,47 @@ internal class CustomExoPlayerView(
it.layoutParams = params it.layoutParams = params
} }
} }
override fun onSingleTap() {
toggleController()
}
override fun onDoubleTapCenterScreen() {
player?.let { player ->
if (player.isPlaying) {
player.pause()
if (!isControllerFullyVisible) showController()
} else {
player.play()
if (isControllerFullyVisible) hideController()
}
}
}
override fun onDoubleTapLeftScreen() {
rewind()
}
override fun onDoubleTapRightScreen() {
forward()
}
override fun onSwipeLeftScreen(distanceY: Float) {
if (!PlayerHelper.swipeGestureEnabled || resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) return
if (isControllerFullyVisible) hideController()
updateBrightness(distanceY)
}
override fun onSwipeRightScreen(distanceY: Float) {
if (!PlayerHelper.swipeGestureEnabled || resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) return
if (isControllerFullyVisible) hideController()
updateVolume(distanceY)
}
override fun onSwipeEnd() {
gestureViewBinding.brightnessControlView.visibility = View.GONE
gestureViewBinding.volumeControlView.visibility = View.GONE
}
} }

View File

@ -0,0 +1,26 @@
package com.github.libretube.ui.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.widget.ConstraintLayout
import com.github.libretube.databinding.PlayerGestureControlsViewBinding
class PlayerGestureControlsView(
context: Context,
attrs: AttributeSet? = null
) : ConstraintLayout(context, attrs) {
var binding: PlayerGestureControlsViewBinding
init {
val layoutInflater = LayoutInflater.from(context)
binding = PlayerGestureControlsViewBinding.inflate(layoutInflater, this, true)
}
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
super.onSizeChanged(width, height, oldHeight, oldHeight)
binding.brightnessProgressBar.max = (height * 0.7).toInt()
binding.volumeProgressBar.max = (height * 0.7).toInt()
}
}

View File

@ -0,0 +1,50 @@
package com.github.libretube.util
import android.content.Context
import android.media.AudioManager
import android.os.Build
import androidx.core.math.MathUtils
import com.github.libretube.extensions.normalize
class AudioHelper(
context: Context,
private val stream: Int = AudioManager.STREAM_MUSIC
) {
private lateinit var audioManager: AudioManager
private var minimumVolumeIndex = 0
private var maximumVolumeIndex = 16
init {
(context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager)?.let {
audioManager = it
maximumVolumeIndex = it.getStreamMaxVolume(stream)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
minimumVolumeIndex = it.getStreamMinVolume(stream)
}
}
}
var volume: Int
get() {
return if (this::audioManager.isInitialized) {
audioManager.getStreamVolume(stream) - minimumVolumeIndex
} else {
0
}
}
set(value) {
if (this::audioManager.isInitialized) {
val vol = MathUtils.clamp(value, minimumVolumeIndex, maximumVolumeIndex)
audioManager.setStreamVolume(stream, vol, 0)
}
}
fun setVolumeWithScale(value: Int, maxValue: Int, minValue: Int = 0) {
volume = value.normalize(minValue, maxValue, minimumVolumeIndex, maximumVolumeIndex)
}
fun getVolumeWithScale(maxValue: Int, minValue: Int = 0): Int {
return volume.normalize(minimumVolumeIndex, maximumVolumeIndex, minValue, maxValue)
}
}

View File

@ -0,0 +1,38 @@
package com.github.libretube.util
import android.app.Activity
import android.view.WindowManager
import com.github.libretube.extensions.normalize
class BrightnessHelper(activity: Activity) {
private val window = activity.window
private val minBrightness = 0.0f
private val maxBrightness = 1.0f
/**
* Wrapper for the current screen brightness
*/
var brightness: Float
get() = window.attributes.screenBrightness
private set(value) {
val lp = window.attributes
lp.screenBrightness = value
window.attributes = lp
}
/**
* Restore screen brightness to device system brightness.
*/
fun resetToSystemBrightness() {
brightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
fun setBrightnessWithScale(value: Float, maxValue: Float, minValue: Float = 0.0f) {
brightness = value.normalize(minValue, maxValue, minBrightness, maxBrightness)
}
fun getBrightnessWithScale(maxValue: Float, minValue: Float = 0.0f): Float {
return brightness.normalize(minBrightness, maxBrightness, minValue, maxValue)
}
}

View File

@ -0,0 +1,130 @@
package com.github.libretube.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import com.github.libretube.ui.interfaces.PlayerGestureOptions
import kotlin.math.abs
class PlayerGestureController(context: Context, private val listner: PlayerGestureOptions) :
View.OnTouchListener {
// width and height should be obtained each time using getter to adopt layout size changes.
private val width get() = Resources.getSystem().displayMetrics.widthPixels
private val height get() = Resources.getSystem().displayMetrics.heightPixels
private val orientation get() = Resources.getSystem().configuration.orientation
private val elapsedTime get() = SystemClock.elapsedRealtime()
private val handler: Handler = Handler(Looper.getMainLooper())
private val gestureDetector: GestureDetector
private var isMoving = false
var isEnabled = true
init {
gestureDetector = GestureDetector(context, GestureListener(), handler)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_UP && isMoving) {
isMoving = false
listner.onSwipeEnd()
}
// Event can be already consumed by some view which may lead to NPE.
try {
gestureDetector.onTouchEvent(event)
} catch (_: Exception) { }
// If orientation is landscape then allow `onScroll` to consume event and return true.
return orientation == Configuration.ORIENTATION_LANDSCAPE
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
private var lastClick = 0L
private var lastDoubleClick = 0L
private var xPos = 0.0F
override fun onDown(e: MotionEvent): Boolean {
if (isMoving) return false
if (isEnabled && isSecondClick()) {
handler.removeCallbacks(runnable)
lastDoubleClick = elapsedTime
val eventPositionPercentageX = xPos / width
when {
eventPositionPercentageX < 0.4 -> listner.onDoubleTapLeftScreen()
eventPositionPercentageX > 0.6 -> listner.onDoubleTapRightScreen()
else -> listner.onDoubleTapCenterScreen()
}
} else {
if (recentDoubleClick()) return true
handler.removeCallbacks(runnable)
handler.postDelayed(runnable, MAX_TIME_DIFF)
lastClick = elapsedTime
xPos = e.x
}
return true
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!isEnabled) return false
val insideThreshHold = abs(e2.y - e1.y) <= MOVEMENT_THRESHOLD
val insideBorder = (e1.x < BORDER_THRESHOLD || e1.y < BORDER_THRESHOLD || e1.x > width - BORDER_THRESHOLD || e1.y > height - BORDER_THRESHOLD)
// If the movement is inside threshold or scroll is horizontal then return false
if (
!isMoving && (
insideThreshHold || insideBorder ||
abs(distanceX) > abs(
distanceY
)
)
) {
return false
}
isMoving = true
when {
width * 0.5 > e1.x -> listner.onSwipeLeftScreen(distanceY)
width * 0.5 < e1.x -> listner.onSwipeRightScreen(distanceY)
}
return true
}
private val runnable = Runnable {
// If user is scrolling then avoid single tap call
if (isMoving || isSecondClick()) return@Runnable
listner.onSingleTap()
}
private fun isSecondClick(): Boolean {
return elapsedTime - lastClick < MAX_TIME_DIFF
}
private fun recentDoubleClick(): Boolean {
return elapsedTime - lastDoubleClick < MAX_TIME_DIFF / 2
}
}
companion object {
private const val MAX_TIME_DIFF = 400L
private const val MOVEMENT_THRESHOLD = 30
private const val BORDER_THRESHOLD = 90
}
}

View File

@ -294,6 +294,12 @@ object PlayerHelper {
true true
) )
val swipeGestureEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.PLAYER_SWIPE_CONTROLS,
true
)
fun getDefaultResolution(context: Context): String { fun getDefaultResolution(context: Context): String {
return if (NetworkHelper.isNetworkMobile(context)) { return if (NetworkHelper.isNetworkMobile(context)) {
PreferenceHelper.getString( PreferenceHelper.getString(

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#4d000000" />
<corners android:radius="8dp" />
<padding
android:bottom="8dp"
android:left="8dp"
android:right="8dp"
android:top="8dp" />
</shape>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@android:color/white"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,15.31L23.31,12 20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69zM12,18V6c3.31,0 6,2.69 6,6s-2.69,6 -6,6z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@android:color/white"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10.85,12.65h2.3L12,9l-1.15,3.65zM20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69L23.31,12 20,8.69zM14.3,16l-0.7,-2h-3.2l-0.7,2H7.8L11,7h2l3.2,9h-1.9z" />
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="427"
android:viewportHeight="427.97">
<path
android:fillColor="#ffffff"
android:pathData="m352.85,188.96h-0.63c-11.65,0 -20.96,9.86 -20.96,22.62v18.05c0,3.87 -3.14,7 -7,7 -3.87,0 -7,-3.13 -7,-7v-33.33c0,-12.76 -9.32,-23.13 -20.97,-23.13 -11.71,-0.01 -21.03,10.38 -21.03,23.13v35.33c0,3.87 -3.14,7 -7,7 -3.87,0 -7,-3.13 -7,-7v-45.03c0,-12.76 -9.98,-22.64 -21.63,-22.64h-0.66c-11.36,0 -20.71,9.36 -20.71,21.67v50.7c0,3.86 -3.14,7 -7,7 -3.87,0 -7,-3.14 -7,-7v-49.23c0,-0.17 -0.02,-0.34 -0.02,-0.51 0,-0.4 0.02,-0.8 0.02,-1.2v-115.11c0,-12.76 -9.68,-23.13 -21.33,-23.13 -11.65,0 -21.32,10.37 -21.34,23.11l-0.13,190.9c0,2.96 -1.86,5.59 -4.64,6.59 -2.79,1 -5.89,0.14 -7.77,-2.15l-22.79,-27.74c-5.93,-7.43 -14.68,-12.06 -24.15,-12.78 -9.27,-0.61 -18.38,2.66 -25.15,9.03 -0.09,0.08 -0.18,0.16 -0.27,0.24l-4.16,3.46 78.84,151.49c12.39,23.81 35.82,38.65 61.14,38.65h91.35c38.61,0 70.05,-33.93 70.09,-75.55 0.02,-22.16 0.04,-38.73 0.06,-52.13 0.05,-35.86 0.06,-49.01 -0.03,-88.51 -0.03,-12.72 -9.51,-22.81 -21.13,-22.81zM352.85,188.96" />
<path
android:fillColor="#ffffff"
android:pathData="m58.83,47.74c2.73,2.73 7.17,2.73 9.9,0s2.73,-7.16 0,-9.9l-25.71,-25.71c-0.68,-2.57 -2.76,-4.53 -5.36,-5.06 -2.61,-0.53 -5.29,0.46 -6.91,2.57 -0.02,0.02 -0.04,0.04 -0.06,0.05l-28.15,28.15c-2.73,2.73 -2.73,7.17 0,9.9 2.73,2.73 7.16,2.73 9.9,0l16.83,-16.83v173.77l-16.83,-16.83c-2.73,-2.73 -7.17,-2.73 -9.9,0 -2.73,2.73 -2.73,7.17 0,9.9l28.15,28.15c2.73,2.73 7.16,2.73 9.9,0l28.15,-28.15c2.73,-2.73 2.73,-7.16 0,-9.9s-7.16,-2.73 -9.9,0l-15.57,15.57v-171.25zM58.83,47.74" />
<path
android:fillColor="#ffffff"
android:pathData="m130.59,66.05c0,-28.75 23.3,-52.05 52.05,-52.05 28.75,0 52.05,23.3 52.05,52.05 0,3.86 3.14,7 7,7 3.87,0 7,-3.14 7,-7 0,-36.48 -29.57,-66.05 -66.05,-66.05 -36.48,0 -66.05,29.57 -66.05,66.05 0,3.86 3.13,7 7,7 3.87,0 7,-3.14 7,-7zM130.59,66.05" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@android:color/white"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@android:color/white"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z" />
</vector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<solid android:color="#40ffffff" />
<corners android:radius="20dip" />
</shape>
</item>
<item android:id="@android:id/progress">
<scale
android:scaleWidth="0%"
android:scaleHeight="100%"
android:scaleGravity="bottom">
<shape>
<solid android:color="?attr/colorPrimary" />
<corners android:radius="20dip" />
</shape>
</scale>
</item>
</layer-list>

View File

@ -19,6 +19,13 @@
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center" /> android:gravity="center" />
<com.github.libretube.ui.views.PlayerGestureControlsView
android:id="@+id/playerGestureControlsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center" />
</com.github.libretube.ui.views.CustomExoPlayerView> </com.github.libretube.ui.views.CustomExoPlayerView>
</LinearLayout> </LinearLayout>

View File

@ -397,6 +397,13 @@
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center" /> android:gravity="center" />
<com.github.libretube.ui.views.PlayerGestureControlsView
android:id="@+id/playerGestureControlsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center" />
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/volumeControlView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="36dp"
android:background="@drawable/controls_layout_bg"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/volume_textView"
style="@style/SwipeControlString"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ProgressBar
android:id="@+id/volume_progressBar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="7dp"
android:layout_height="100dp"
android:layout_marginVertical="4dp"
android:progressDrawable="@drawable/vertical_progressbar" />
<ImageView
android:id="@+id/volume_imageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/volume"
android:src="@drawable/ic_volume_up" />
</LinearLayout>
<LinearLayout
android:id="@+id/brightnessControlView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="36dp"
android:background="@drawable/controls_layout_bg"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/brightness_textView"
style="@style/SwipeControlString"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ProgressBar
android:id="@+id/brightness_progressBar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="7dp"
android:layout_height="100dp"
android:layout_marginVertical="4dp"
android:progressDrawable="@drawable/vertical_progressbar" />
<ImageView
android:id="@+id/brightness_imageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/brightness"
android:src="@drawable/ic_brightness" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -389,6 +389,11 @@
<string name="local_playlists">Local playlists</string> <string name="local_playlists">Local playlists</string>
<string name="not_enabled">Menu item not enabled!</string> <string name="not_enabled">Menu item not enabled!</string>
<string name="select_other_start_tab">Please select an other start tab first!</string> <string name="select_other_start_tab">Please select an other start tab first!</string>
<string name="brightness">Brightness</string>
<string name="volume">Volume</string>
<string name="auto">Auto</string>
<string name="swipe_controls">Swipe controls</string>
<string name="swipe_controls_summary">Use swipe gesture to adjust the brightness and volume.</string>
<!-- Notification channel strings --> <!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string> <string name="download_channel_name">Download Service</string>

View File

@ -149,6 +149,18 @@
<item name="android:textColor">@android:color/white</item> <item name="android:textColor">@android:color/white</item>
</style> </style>
<style name="SwipeControlString">
<item name="android:gravity">center</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
<item name="paddingEnd">5dp</item>
<item name="android:textSize">14sp</item>
<item name="android:textStyle">normal</item>
<item name="android:textColor">@android:color/white</item>
<item name="android:text">0</item>
</style>
<style name="Chip" parent="Widget.Material3.Chip.Suggestion"> <style name="Chip" parent="Widget.Material3.Chip.Suggestion">
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>

View File

@ -85,6 +85,13 @@
app:key="picture_in_picture" app:key="picture_in_picture"
app:title="@string/picture_in_picture" /> app:title="@string/picture_in_picture" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:icon="@drawable/ic_swipe_gesture"
android:summary="@string/swipe_controls_summary"
app:key="player_swipe_controls"
app:title="@string/swipe_controls" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:icon="@drawable/ic_pause_filled" android:icon="@drawable/ic_pause_filled"
android:summary="@string/pauseOnScreenOff_summary" android:summary="@string/pauseOnScreenOff_summary"