Improve player gestures and add pinch to zoom (#74)
* implemented pan/panStop on a GestureListener to detect the entry location and exclude top status bar area. * remember last brightness set for players [ Exoplayer , MPV] * Pinch to zoom and auto rotate feature for Exoplayer * Only save the brightness when the overlay disappears instead of on every scroll Also clean up some files * removed unnecessary configChanges from Manifest Co-authored-by: Jarne Demeulemeester <jarnedemeulemeester@gmail.com>
This commit is contained in:
parent
6f2c9945b5
commit
7d9857d3ce
7 changed files with 161 additions and 5 deletions
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PlayerActivity"
|
android:name=".PlayerActivity"
|
||||||
android:screenOrientation="userLandscape" />
|
android:screenOrientation="sensorLandscape" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".tv.TvPlayerActivity"
|
android:name=".tv.TvPlayerActivity"
|
||||||
|
|
|
@ -18,13 +18,18 @@ import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment
|
||||||
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
|
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
|
||||||
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
||||||
import dev.jdtech.jellyfin.mpv.TrackType
|
import dev.jdtech.jellyfin.mpv.TrackType
|
||||||
|
import dev.jdtech.jellyfin.utils.AppPreferences
|
||||||
import dev.jdtech.jellyfin.utils.PlayerGestureHelper
|
import dev.jdtech.jellyfin.utils.PlayerGestureHelper
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PlayerActivity : BasePlayerActivity() {
|
class PlayerActivity : BasePlayerActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appPreferences: AppPreferences
|
||||||
|
|
||||||
lateinit var binding: ActivityPlayerBinding
|
lateinit var binding: ActivityPlayerBinding
|
||||||
private lateinit var playerGestureHelper: PlayerGestureHelper
|
private lateinit var playerGestureHelper: PlayerGestureHelper
|
||||||
override val viewModel: PlayerActivityViewModel by viewModels()
|
override val viewModel: PlayerActivityViewModel by viewModels()
|
||||||
|
@ -44,7 +49,12 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
|
|
||||||
configureInsets(playerControls)
|
configureInsets(playerControls)
|
||||||
|
|
||||||
playerGestureHelper = PlayerGestureHelper(this, binding.playerView, getSystemService(Context.AUDIO_SERVICE) as AudioManager)
|
playerGestureHelper = PlayerGestureHelper(
|
||||||
|
appPreferences,
|
||||||
|
this,
|
||||||
|
binding.playerView,
|
||||||
|
getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
)
|
||||||
|
|
||||||
binding.playerView.findViewById<View>(R.id.back_button).setOnClickListener {
|
binding.playerView.findViewById<View>(R.id.back_button).setOnClickListener {
|
||||||
onBackPressed()
|
onBackPressed()
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package dev.jdtech.jellyfin.utils
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class AppPreferences
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val sharedPreferences: SharedPreferences
|
||||||
|
) {
|
||||||
|
var playerBrightness: Float
|
||||||
|
get() = sharedPreferences.getFloat(
|
||||||
|
Constants.PREF_PLAYER_BRIGHTNESS,
|
||||||
|
BRIGHTNESS_OVERRIDE_NONE
|
||||||
|
)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putFloat(Constants.PREF_PLAYER_BRIGHTNESS, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
app/src/main/java/dev/jdtech/jellyfin/utils/Constants.kt
Normal file
14
app/src/main/java/dev/jdtech/jellyfin/utils/Constants.kt
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.jdtech.jellyfin.utils
|
||||||
|
|
||||||
|
object Constants {
|
||||||
|
|
||||||
|
//player
|
||||||
|
const val GESTURE_EXCLUSION_AREA_TOP = 48
|
||||||
|
const val FULL_SWIPE_RANGE_SCREEN_RATIO = 0.66f
|
||||||
|
const val ZOOM_SCALE_BASE = 1f
|
||||||
|
const val ZOOM_SCALE_THRESHOLD = 0.01f
|
||||||
|
|
||||||
|
//pref
|
||||||
|
const val PREF_PLAYER_BRIGHTNESS = "pref_player_brightness"
|
||||||
|
|
||||||
|
}
|
|
@ -4,24 +4,36 @@ import android.media.AudioManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.GestureDetector
|
import android.view.GestureDetector
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import android.view.ScaleGestureDetector
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
|
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
|
||||||
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF
|
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF
|
||||||
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||||
import com.google.android.exoplayer2.ui.PlayerView
|
import com.google.android.exoplayer2.ui.PlayerView
|
||||||
import dev.jdtech.jellyfin.PlayerActivity
|
import dev.jdtech.jellyfin.PlayerActivity
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class PlayerGestureHelper(
|
class PlayerGestureHelper(
|
||||||
|
private val appPreferences: AppPreferences,
|
||||||
private val activity: PlayerActivity,
|
private val activity: PlayerActivity,
|
||||||
private val playerView: PlayerView,
|
private val playerView: PlayerView,
|
||||||
private val audioManager: AudioManager
|
private val audioManager: AudioManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks whether video content should fill the screen, cutting off unwanted content on the sides.
|
||||||
|
* Useful on wide-screen phones to remove black bars from some movies.
|
||||||
|
*/
|
||||||
|
private var isZoomEnabled = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks a value during a swipe gesture (between multiple onScroll calls).
|
* 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
|
* When the gesture starts it's reset to an initial value and gets increased or decreased
|
||||||
* (depending on the direction) as the gesture progresses.
|
* (depending on the direction) as the gesture progresses.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private var swipeGestureValueTrackerVolume = -1f
|
private var swipeGestureValueTrackerVolume = -1f
|
||||||
private var swipeGestureValueTrackerBrightness = -1f
|
private var swipeGestureValueTrackerBrightness = -1f
|
||||||
|
|
||||||
|
@ -42,10 +54,14 @@ class PlayerGestureHelper(
|
||||||
if (abs(distanceY / distanceX) < 2)
|
if (abs(distanceY / distanceX) < 2)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
if (firstEvent.y < playerView.resources.dip(Constants.GESTURE_EXCLUSION_AREA_TOP))
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
val viewCenterX = playerView.measuredWidth / 2
|
val viewCenterX = playerView.measuredWidth / 2
|
||||||
|
|
||||||
// Distance to swipe to go from min to max
|
// Distance to swipe to go from min to max
|
||||||
val distanceFull = playerView.measuredHeight
|
val distanceFull = playerView.measuredHeight * Constants.FULL_SWIPE_RANGE_SCREEN_RATIO
|
||||||
val ratioChange = distanceY / distanceFull
|
val ratioChange = distanceY / distanceFull
|
||||||
|
|
||||||
if (firstEvent.x.toInt() > viewCenterX) {
|
if (firstEvent.x.toInt() > viewCenterX) {
|
||||||
|
@ -99,14 +115,39 @@ class PlayerGestureHelper(
|
||||||
|
|
||||||
private val hideGestureBrightnessIndicatorOverlayAction = Runnable {
|
private val hideGestureBrightnessIndicatorOverlayAction = Runnable {
|
||||||
activity.binding.gestureBrightnessLayout.visibility = View.GONE
|
activity.binding.gestureBrightnessLayout.visibility = View.GONE
|
||||||
|
appPreferences.playerBrightness = activity.window.attributes.screenBrightness
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles scale/zoom gesture
|
||||||
|
*/
|
||||||
|
private val zoomGestureDetector = ScaleGestureDetector(playerView.context, object : ScaleGestureDetector.OnScaleGestureListener {
|
||||||
|
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true
|
||||||
|
|
||||||
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||||
|
val scaleFactor = detector.scaleFactor
|
||||||
|
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {
|
||||||
|
isZoomEnabled = scaleFactor > 1
|
||||||
|
updateZoomMode(isZoomEnabled)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScaleEnd(detector: ScaleGestureDetector) = Unit
|
||||||
|
}).apply { isQuickScaleEnabled = false }
|
||||||
|
|
||||||
|
private fun updateZoomMode(enabled: Boolean) {
|
||||||
|
playerView.resizeMode = if (enabled) AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
activity.window.attributes.screenBrightness = appPreferences.playerBrightness
|
||||||
@Suppress("ClickableViewAccessibility")
|
@Suppress("ClickableViewAccessibility")
|
||||||
playerView.setOnTouchListener { _, event ->
|
playerView.setOnTouchListener { _, event ->
|
||||||
if (playerView.useController) {
|
if (playerView.useController) {
|
||||||
when (event.pointerCount) {
|
when (event.pointerCount) {
|
||||||
1 -> gestureDetector.onTouchEvent(event)
|
1 -> gestureDetector.onTouchEvent(event)
|
||||||
|
2 -> zoomGestureDetector.onTouchEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(event.action == MotionEvent.ACTION_UP) {
|
if(event.action == MotionEvent.ACTION_UP) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.jdtech.jellyfin.utils
|
package dev.jdtech.jellyfin.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -35,4 +36,7 @@ fun Fragment.checkIfLoginRequired(error: String) {
|
||||||
|
|
||||||
|
|
||||||
inline fun Context.toast(@StringRes text: Int, duration: Int = Toast.LENGTH_SHORT) =
|
inline fun Context.toast(@StringRes text: Int, duration: Int = Toast.LENGTH_SHORT) =
|
||||||
Toast.makeText(this, text, duration).show()
|
Toast.makeText(this, text, duration).show()
|
||||||
|
|
||||||
|
|
||||||
|
inline fun Resources.dip(px: Int) = (px * displayMetrics.density).toInt()
|
63
app/src/main/res/layout/exo_player_view.xml
Normal file
63
app/src/main/res/layout/exo_player_view.xml
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
tools:keep="@layout/exo_styled_player_view">
|
||||||
|
|
||||||
|
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout android:id="@id/exo_content_frame"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<!-- Video surface will be inserted as the first child of the content frame. -->
|
||||||
|
|
||||||
|
<View android:id="@id/exo_shutter"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@android:color/black"/>
|
||||||
|
|
||||||
|
<ImageView android:id="@id/exo_artwork"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="fitXY"/>
|
||||||
|
|
||||||
|
<ProgressBar android:id="@id/exo_buffering"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:layout_gravity="center"/>
|
||||||
|
|
||||||
|
<TextView android:id="@id/exo_error_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginBottom="@dimen/exo_error_message_margin_bottom"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="@color/exo_white"
|
||||||
|
android:textSize="@dimen/exo_error_message_text_size"
|
||||||
|
android:lineSpacingMultiplier="1.1"
|
||||||
|
android:background="@drawable/exo_rounded_rectangle"
|
||||||
|
android:paddingLeft="@dimen/exo_error_message_text_padding_horizontal"
|
||||||
|
android:paddingRight="@dimen/exo_error_message_text_padding_horizontal"
|
||||||
|
android:paddingTop="@dimen/exo_error_message_text_padding_vertical"
|
||||||
|
android:paddingBottom="@dimen/exo_error_message_text_padding_vertical"/>
|
||||||
|
|
||||||
|
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
|
||||||
|
|
||||||
|
<com.google.android.exoplayer2.ui.SubtitleView android:id="@id/exo_subtitles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
<FrameLayout android:id="@id/exo_ad_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
<FrameLayout android:id="@id/exo_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
<View android:id="@id/exo_controller_placeholder"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
</merge>
|
Loading…
Reference in a new issue