Add brightness swipe controls (#69)
* Add brightness swipe control * Add background to the overlays Co-authored-by: Jarne Demeulemeester <32322857+jarnedemeulemeester@users.noreply.github.com>
This commit is contained in:
parent
598c11f299
commit
e259c405bb
9 changed files with 326 additions and 81 deletions
|
@ -1,6 +1,7 @@
|
|||
package dev.jdtech.jellyfin
|
||||
|
||||
import android.os.Build
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
|
@ -17,19 +18,17 @@ import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment
|
|||
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
|
||||
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
||||
import dev.jdtech.jellyfin.mpv.TrackType
|
||||
import dev.jdtech.jellyfin.utils.AudioController
|
||||
import dev.jdtech.jellyfin.utils.VerticalSwipeListener
|
||||
import dev.jdtech.jellyfin.utils.PlayerGestureHelper
|
||||
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
||||
import timber.log.Timber
|
||||
import kotlin.math.max
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PlayerActivity : BasePlayerActivity() {
|
||||
|
||||
private lateinit var binding: ActivityPlayerBinding
|
||||
lateinit var binding: ActivityPlayerBinding
|
||||
private lateinit var playerGestureHelper: PlayerGestureHelper
|
||||
override val viewModel: PlayerActivityViewModel by viewModels()
|
||||
private val args: PlayerActivityArgs by navArgs()
|
||||
private val audioController by lazy { AudioController(this) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -42,10 +41,11 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
binding.playerView.player = viewModel.player
|
||||
|
||||
val playerControls = binding.playerView.findViewById<View>(R.id.player_controls)
|
||||
setupVolumeControl()
|
||||
|
||||
configureInsets(playerControls)
|
||||
|
||||
playerGestureHelper = PlayerGestureHelper(this, binding.playerView, getSystemService(Context.AUDIO_SERVICE) as AudioManager)
|
||||
|
||||
binding.playerView.findViewById<View>(R.id.back_button).setOnClickListener {
|
||||
onBackPressed()
|
||||
}
|
||||
|
@ -158,19 +158,5 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
viewModel.initializePlayer(args.items)
|
||||
hideSystemUI()
|
||||
}
|
||||
|
||||
private fun setupVolumeControl() {
|
||||
val height = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
windowManager.currentWindowMetrics.bounds.height()
|
||||
} else {
|
||||
windowManager.defaultDisplay.height
|
||||
}
|
||||
binding.playerView.setOnTouchListener(VerticalSwipeListener(
|
||||
onUp = { audioController.volumeUp() },
|
||||
onDown = { audioController.volumeDown() },
|
||||
onTouch = { audioController.showVolumeSlider() },
|
||||
threshold = max(height / 8, 100)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
package dev.jdtech.jellyfin.utils
|
||||
|
||||
import android.media.AudioManager
|
||||
import android.provider.Settings
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
|
||||
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF
|
||||
import com.google.android.exoplayer2.ui.PlayerView
|
||||
import dev.jdtech.jellyfin.PlayerActivity
|
||||
import timber.log.Timber
|
||||
import kotlin.math.abs
|
||||
|
||||
class PlayerGestureHelper(
|
||||
private val activity: PlayerActivity,
|
||||
private val playerView: PlayerView,
|
||||
private val audioManager: AudioManager
|
||||
) {
|
||||
/**
|
||||
* Tracks a value during a swipe gesture (between multiple onScroll calls).
|
||||
* When the gesture starts it's reset to an initial value and gets increased or decreased
|
||||
* (depending on the direction) as the gesture progresses.
|
||||
*/
|
||||
private var swipeGestureValueTrackerVolume = -1f
|
||||
private var swipeGestureValueTrackerBrightness = -1f
|
||||
|
||||
private val gestureDetector = GestureDetector(playerView.context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
||||
playerView.apply {
|
||||
if (!isControllerVisible) showController() else hideController()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(firstEvent: MotionEvent, currentEvent: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
||||
// Check whether swipe was oriented vertically
|
||||
if (abs(distanceY / distanceX) < 2)
|
||||
return false
|
||||
|
||||
val viewCenterX = playerView.measuredWidth / 2
|
||||
|
||||
// Distance to swipe to go from min to max
|
||||
val distanceFull = playerView.measuredHeight
|
||||
val ratioChange = distanceY / distanceFull
|
||||
|
||||
if (firstEvent.x.toInt() > viewCenterX) {
|
||||
// Swiping on the right, change volume
|
||||
|
||||
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
if (swipeGestureValueTrackerVolume == -1f) swipeGestureValueTrackerVolume = currentVolume.toFloat()
|
||||
|
||||
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||
val change = ratioChange * maxVolume
|
||||
swipeGestureValueTrackerVolume += change
|
||||
|
||||
val toSet = swipeGestureValueTrackerVolume.toInt().coerceIn(0, maxVolume)
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, toSet, 0)
|
||||
|
||||
activity.binding.gestureVolumeLayout.visibility = View.VISIBLE
|
||||
activity.binding.gestureVolumeProgressBar.max = maxVolume
|
||||
activity.binding.gestureVolumeProgressBar.progress = toSet
|
||||
activity.binding.gestureVolumeText.text = "${(toSet.toFloat()/maxVolume.toFloat()).times(100).toInt()}%"
|
||||
} else {
|
||||
// Swiping on the left, change brightness
|
||||
val window = activity.window
|
||||
val brightnessRange = BRIGHTNESS_OVERRIDE_OFF..BRIGHTNESS_OVERRIDE_FULL
|
||||
|
||||
// Initialize on first swipe
|
||||
if (swipeGestureValueTrackerBrightness == -1f) {
|
||||
val brightness = window.attributes.screenBrightness
|
||||
Timber.d("Brightness ${Settings.System.getFloat(activity.contentResolver, Settings.System.SCREEN_BRIGHTNESS)}")
|
||||
swipeGestureValueTrackerBrightness = when (brightness) {
|
||||
in brightnessRange -> brightness
|
||||
else -> Settings.System.getFloat(activity.contentResolver, Settings.System.SCREEN_BRIGHTNESS)/255
|
||||
}
|
||||
}
|
||||
swipeGestureValueTrackerBrightness = (swipeGestureValueTrackerBrightness + ratioChange).coerceIn(brightnessRange)
|
||||
val lp = window.attributes
|
||||
lp.screenBrightness = swipeGestureValueTrackerBrightness
|
||||
window.attributes = lp
|
||||
|
||||
activity.binding.gestureBrightnessLayout.visibility = View.VISIBLE
|
||||
activity.binding.gestureBrightnessProgressBar.max = BRIGHTNESS_OVERRIDE_FULL.times(100).toInt()
|
||||
activity.binding.gestureBrightnessProgressBar.progress = lp.screenBrightness.times(100).toInt()
|
||||
activity.binding.gestureBrightnessText.text = "${(lp.screenBrightness/BRIGHTNESS_OVERRIDE_FULL).times(100).toInt()}%"
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
private val hideGestureVolumeIndicatorOverlayAction = Runnable {
|
||||
activity.binding.gestureVolumeLayout.visibility = View.GONE
|
||||
}
|
||||
|
||||
private val hideGestureBrightnessIndicatorOverlayAction = Runnable {
|
||||
activity.binding.gestureBrightnessLayout.visibility = View.GONE
|
||||
}
|
||||
|
||||
init {
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
playerView.setOnTouchListener { _, event ->
|
||||
if (playerView.useController) {
|
||||
when (event.pointerCount) {
|
||||
1 -> gestureDetector.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
if(event.action == MotionEvent.ACTION_UP) {
|
||||
activity.binding.gestureVolumeLayout.apply {
|
||||
if (visibility == View.VISIBLE) {
|
||||
removeCallbacks(hideGestureVolumeIndicatorOverlayAction)
|
||||
postDelayed(hideGestureVolumeIndicatorOverlayAction, 1000)
|
||||
}
|
||||
}
|
||||
activity.binding.gestureBrightnessLayout.apply {
|
||||
if (visibility == View.VISIBLE) {
|
||||
removeCallbacks(hideGestureBrightnessIndicatorOverlayAction)
|
||||
postDelayed(hideGestureBrightnessIndicatorOverlayAction, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package dev.jdtech.jellyfin.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MotionEvent
|
||||
import android.view.MotionEvent.ACTION_DOWN
|
||||
import android.view.MotionEvent.ACTION_MOVE
|
||||
import android.view.MotionEvent.ACTION_UP
|
||||
import android.view.View
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
/**
|
||||
* Never consumes touch events. Can report when swipe starts with onTouch.
|
||||
*/
|
||||
internal class VerticalSwipeListener(
|
||||
private val onUp: () -> Unit,
|
||||
private val onDown: () -> Unit,
|
||||
private val onTouch: () -> Unit = {},
|
||||
private val threshold: Int = 150,
|
||||
): View.OnTouchListener {
|
||||
|
||||
private var verticalStart = 0f
|
||||
private var distance = 0f
|
||||
private var dispatchTouchEvent = true
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||
when(event.action) {
|
||||
ACTION_DOWN -> verticalStart = event.y
|
||||
ACTION_UP -> dispatchTouchEvent = true
|
||||
ACTION_MOVE -> event.reportSwipe()
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun MotionEvent.reportSwipe() {
|
||||
reportFirstTouch()
|
||||
|
||||
distance = verticalStart - y
|
||||
if (distance.tooShort()) return
|
||||
|
||||
verticalStart = y
|
||||
|
||||
if (distance > 0) {
|
||||
onUp()
|
||||
} else {
|
||||
onDown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportFirstTouch() {
|
||||
if (dispatchTouchEvent) {
|
||||
onTouch()
|
||||
dispatchTouchEvent = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun Float.tooShort() = absoluteValue < threshold
|
||||
}
|
69
app/src/main/res/drawable/ic_sun.xml
Normal file
69
app/src/main/res/drawable/ic_sun.xml
Normal file
|
@ -0,0 +1,69 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,2v2"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,20v2"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M5,5l1.5,1.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M17.5,17.5L19,19"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M2,12h2"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M20,12h2"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M5,19l1.5,-1.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M17.5,6.5L19,5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
20
app/src/main/res/drawable/ic_volume.xml
Normal file
20
app/src/main/res/drawable/ic_volume.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M11,5l-5,4l-4,0l0,6l4,0l5,4l0,-14z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M19.07,4.93a10,10 0,0 1,0 14.14M15.54,8.46a5,5 0,0 1,0 7.07"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
6
app/src/main/res/drawable/overlay_background.xml
Normal file
6
app/src/main/res/drawable/overlay_background.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/player_background" />
|
||||
<corners android:radius="10dp" />
|
||||
</shape>
|
19
app/src/main/res/drawable/progress_scale_drawable.xml
Normal file
19
app/src/main/res/drawable/progress_scale_drawable.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?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="@color/transparent_white_300"/>
|
||||
<corners android:radius="100dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:id="@android:id/progress">
|
||||
<scale android:scaleGravity="bottom" android:scaleWidth="0%" android:scaleHeight="100%">
|
||||
<shape >
|
||||
<solid android:color="@color/white"/>
|
||||
<corners android:radius="100dp"/>
|
||||
</shape>
|
||||
</scale>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -13,4 +13,78 @@
|
|||
android:background="@color/black"
|
||||
app:show_buffering="always" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/gesture_volume_layout"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:clickable="false"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="16dp"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/overlay_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gesture_volume_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginVertical="16dp"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/white"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/gesture_volume_progress_bar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="125dp"
|
||||
android:progressDrawable="@drawable/progress_scale_drawable"
|
||||
android:progress="58"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/gesture_volume_image"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginVertical="16dp"
|
||||
android:src="@drawable/ic_volume"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/gesture_brightness_layout"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:clickable="false"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="16dp"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/overlay_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gesture_brightness_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginVertical="16dp"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/white"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/gesture_brightness_progress_bar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="125dp"
|
||||
android:progressDrawable="@drawable/progress_scale_drawable"
|
||||
android:progress="58"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/gesture_brightness_image"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginVertical="16dp"
|
||||
android:src="@drawable/ic_sun"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
<color name="neutral_300">#A9BDD1</color>
|
||||
<color name="neutral_200">#D7E0E9</color>
|
||||
<color name="neutral_100">#EEF2F6</color>
|
||||
<color name="transparent_white_300">#4DFFFFFF</color>
|
||||
<color name="transparent_black_300">#4D000000</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="red">#EB5757</color>
|
||||
|
|
Loading…
Reference in a new issue