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:
maulik9898 2022-01-08 22:34:28 +05:30 committed by GitHub
parent 6f2c9945b5
commit 7d9857d3ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 161 additions and 5 deletions

View file

@ -21,7 +21,7 @@
<activity
android:name=".PlayerActivity"
android:screenOrientation="userLandscape" />
android:screenOrientation="sensorLandscape" />
<activity
android:name=".tv.TvPlayerActivity"

View file

@ -18,13 +18,18 @@ 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.AppPreferences
import dev.jdtech.jellyfin.utils.PlayerGestureHelper
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class PlayerActivity : BasePlayerActivity() {
@Inject
lateinit var appPreferences: AppPreferences
lateinit var binding: ActivityPlayerBinding
private lateinit var playerGestureHelper: PlayerGestureHelper
override val viewModel: PlayerActivityViewModel by viewModels()
@ -44,7 +49,12 @@ class PlayerActivity : BasePlayerActivity() {
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 {
onBackPressed()

View file

@ -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)
}
}
}

View 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"
}

View file

@ -4,24 +4,36 @@ import android.media.AudioManager
import android.provider.Settings
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
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.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.PlayerView
import dev.jdtech.jellyfin.PlayerActivity
import timber.log.Timber
import kotlin.math.abs
class PlayerGestureHelper(
private val appPreferences: AppPreferences,
private val activity: PlayerActivity,
private val playerView: PlayerView,
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).
* 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
@ -42,10 +54,14 @@ class PlayerGestureHelper(
if (abs(distanceY / distanceX) < 2)
return false
if (firstEvent.y < playerView.resources.dip(Constants.GESTURE_EXCLUSION_AREA_TOP))
return false
val viewCenterX = playerView.measuredWidth / 2
// 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
if (firstEvent.x.toInt() > viewCenterX) {
@ -99,14 +115,39 @@ class PlayerGestureHelper(
private val hideGestureBrightnessIndicatorOverlayAction = Runnable {
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 {
activity.window.attributes.screenBrightness = appPreferences.playerBrightness
@Suppress("ClickableViewAccessibility")
playerView.setOnTouchListener { _, event ->
if (playerView.useController) {
when (event.pointerCount) {
1 -> gestureDetector.onTouchEvent(event)
2 -> zoomGestureDetector.onTouchEvent(event)
}
}
if(event.action == MotionEvent.ACTION_UP) {

View file

@ -1,6 +1,7 @@
package dev.jdtech.jellyfin.utils
import android.content.Context
import android.content.res.Resources
import android.widget.Toast
import androidx.annotation.StringRes
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) =
Toast.makeText(this, text, duration).show()
Toast.makeText(this, text, duration).show()
inline fun Resources.dip(px: Int) = (px * displayMetrics.density).toInt()

View 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>