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 USE_HLS_OVER_DASH = "use_hls"
const val QUEUE_AUTO_INSERT_RELATED = "queue_insert_related_videos"
const val PLAYER_SWIPE_CONTROLS = "player_swipe_controls"
/**
* 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(
null,
binding.doubleTapOverlay.binding,
binding.playerGestureControlsView.binding,
null
)
}

View File

@ -49,6 +49,7 @@ import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.databinding.FragmentPlayerBinding
import com.github.libretube.databinding.PlayerGestureControlsViewBinding
import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.WatchPosition
@ -118,6 +119,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
lateinit var binding: FragmentPlayerBinding
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
private lateinit var doubleTapOverlayBinding: DoubleTapOverlayBinding
private lateinit var playerGestureControlsViewBinding: PlayerGestureControlsViewBinding
private val viewModel: PlayerViewModel by activityViewModels()
/**
@ -183,6 +185,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
exoPlayerView = binding.player
playerBinding = binding.player.binding
doubleTapOverlayBinding = binding.doubleTapOverlay.binding
playerGestureControlsViewBinding = binding.playerGestureControlsView.binding
// Inflate the layout for this fragment
return binding.root
@ -775,6 +778,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
binding.player.initialize(
this,
doubleTapOverlayBinding,
playerGestureControlsViewBinding,
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
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.os.Handler
@ -8,18 +9,24 @@ import android.os.Looper
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import com.github.libretube.R
import com.github.libretube.databinding.DoubleTapOverlayBinding
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.obj.BottomSheetItem
import com.github.libretube.ui.activities.MainActivity
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.PlayerGestureOptions
import com.github.libretube.ui.interfaces.PlayerOptions
import com.github.libretube.ui.sheets.BaseBottomSheet
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.PlayingQueue
import com.google.android.exoplayer2.PlaybackParameters
@ -33,8 +40,16 @@ import com.google.android.exoplayer2.util.RepeatModeUtil
internal class CustomExoPlayerView(
context: Context,
attributeSet: AttributeSet? = null
) : StyledPlayerView(context, attributeSet), PlayerOptions {
) : StyledPlayerView(context, attributeSet), PlayerOptions, PlayerGestureOptions {
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
/**
@ -45,16 +60,12 @@ internal class CustomExoPlayerView(
private val runnableHandler = Handler(Looper.getMainLooper())
// the x-position of where the user clicked
private var xPos = 0F
var isPlayerLocked: Boolean = false
/**
* Preferences
*/
var autoplayEnabled = PlayerHelper.autoPlayEnabled
private var doubleTapAllowed = true
private var resizeModePref = PlayerHelper.resizeModePref
@ -65,42 +76,23 @@ internal class CustomExoPlayerView(
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(
playerViewInterface: OnlinePlayerOptions?,
doubleTapOverlayBinding: DoubleTapOverlayBinding,
playerGestureControlsViewBinding: PlayerGestureControlsViewBinding,
trackSelector: TrackSelector?
) {
this.playerOptionsInterface = playerViewInterface
this.doubleTapOverlayBinding = doubleTapOverlayBinding
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
setOnClickListener(doubleTouchListener)
// Set touch listner for tap and swipe gestures.
setOnTouchListener(playerGestureController)
initializeGestureProgress()
enableDoubleTapToSeek()
initializeAdvancedOptions(context)
@ -144,10 +136,6 @@ internal class CustomExoPlayerView(
}
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
}
@ -261,8 +249,8 @@ internal class CustomExoPlayerView(
binding.exoBottomBar.visibility = visibility
binding.closeImageButton.visibility = visibility
// disable double tap to seek if the player is locked
doubleTapAllowed = !isLocked
// disable tap and swipe gesture if the player is locked
playerGestureController.isEnabled = isLocked
}
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() {
// autoplay options dialog
BaseBottomSheet()
@ -408,4 +446,47 @@ internal class CustomExoPlayerView(
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
)
val swipeGestureEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.PLAYER_SWIPE_CONTROLS,
true
)
fun getDefaultResolution(context: Context): String {
return if (NetworkHelper.isNetworkMobile(context)) {
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: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>
</LinearLayout>

View File

@ -397,6 +397,13 @@
android:layout_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
android:layout_width="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="not_enabled">Menu item not enabled!</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 -->
<string name="download_channel_name">Download Service</string>

View File

@ -149,6 +149,18 @@
<item name="android:textColor">@android:color/white</item>
</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">
<item name="android:layout_height">wrap_content</item>

View File

@ -85,6 +85,13 @@
app:key="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
android:icon="@drawable/ic_pause_filled"
android:summary="@string/pauseOnScreenOff_summary"