NewPipe/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt

521 lines
17 KiB
Kotlin
Raw Normal View History

2020-10-21 16:40:22 +02:00
package org.schabi.newpipe.player.event
import android.content.Context
import android.os.Handler
2020-10-21 16:40:22 +02:00
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import org.schabi.newpipe.ktx.animate
2020-10-21 16:40:22 +02:00
import org.schabi.newpipe.player.MainPlayer
2021-01-08 18:35:33 +01:00
import org.schabi.newpipe.player.Player
2020-10-21 16:40:22 +02:00
import org.schabi.newpipe.player.helper.PlayerHelper
2021-01-08 18:35:33 +01:00
import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs
2020-10-21 16:40:22 +02:00
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
2020-11-18 23:45:19 +01:00
import kotlin.math.min
2020-10-21 16:40:22 +02:00
/**
2021-01-08 18:35:33 +01:00
* Base gesture handling for [Player]
2020-10-21 16:40:22 +02:00
*
* This class contains the logic for the player gestures like View preparations
* and provides some abstract methods to make it easier separating the logic from the UI.
*/
abstract class BasePlayerGestureListener(
@JvmField
2021-01-08 18:35:33 +01:00
protected val player: Player,
2020-10-21 16:40:22 +02:00
@JvmField
protected val service: MainPlayer
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
// ///////////////////////////////////////////////////////////////////
// Abstract methods for VIDEO and POPUP
// ///////////////////////////////////////////////////////////////////
abstract fun onDoubleTap(event: MotionEvent, portion: DisplayPortion)
abstract fun onSingleTap(playerType: MainPlayer.PlayerType)
abstract fun onScroll(
playerType: MainPlayer.PlayerType,
portion: DisplayPortion,
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
)
abstract fun onScrollEnd(playerType: MainPlayer.PlayerType, event: MotionEvent)
// ///////////////////////////////////////////////////////////////////
// Abstract methods for POPUP (exclusive)
// ///////////////////////////////////////////////////////////////////
abstract fun onPopupResizingStart()
abstract fun onPopupResizingEnd()
private var initialPopupX: Int = -1
private var initialPopupY: Int = -1
private var isMovingInMain = false
private var isMovingInPopup = false
private var isResizing = false
private val tossFlingVelocity = PlayerHelper.getTossFlingVelocity()
2020-10-21 16:40:22 +02:00
// [popup] initial coordinates and distance between fingers
private var initPointerDistance = -1.0
private var initFirstPointerX = -1f
private var initFirstPointerY = -1f
private var initSecPointerX = -1f
private var initSecPointerY = -1f
// ///////////////////////////////////////////////////////////////////
// onTouch implementation
// ///////////////////////////////////////////////////////////////////
override fun onTouch(v: View, event: MotionEvent): Boolean {
2021-01-08 18:35:33 +01:00
return if (player.popupPlayerSelected()) {
2020-10-21 16:40:22 +02:00
onTouchInPopup(v, event)
} else {
onTouchInMain(v, event)
}
}
private fun onTouchInMain(v: View, event: MotionEvent): Boolean {
2021-01-08 18:35:33 +01:00
player.gestureDetector.onTouchEvent(event)
2020-10-21 16:40:22 +02:00
if (event.action == MotionEvent.ACTION_UP && isMovingInMain) {
isMovingInMain = false
onScrollEnd(MainPlayer.PlayerType.VIDEO, event)
}
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
2021-01-08 18:35:33 +01:00
v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen)
2020-10-21 16:40:22 +02:00
true
}
MotionEvent.ACTION_UP -> {
v.parent.requestDisallowInterceptTouchEvent(false)
false
}
else -> true
}
}
private fun onTouchInPopup(v: View, event: MotionEvent): Boolean {
2021-01-08 18:35:33 +01:00
player.gestureDetector.onTouchEvent(event)
2020-10-21 16:40:22 +02:00
if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
}
onPopupResizingStart()
// record coordinates of fingers
initFirstPointerX = event.getX(0)
initFirstPointerY = event.getY(0)
initSecPointerX = event.getX(1)
initSecPointerY = event.getY(1)
// record distance between fingers
2020-11-04 18:07:40 +01:00
initPointerDistance = hypot(
initFirstPointerX - initSecPointerX.toDouble(),
initFirstPointerY - initSecPointerY.toDouble()
)
2020-10-21 16:40:22 +02:00
isResizing = true
}
if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
if (DEBUG) {
2020-11-04 18:07:40 +01:00
Log.d(
TAG,
"onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
"[${event.rawX}, ${event.rawY}]"
)
2020-10-21 16:40:22 +02:00
}
return handleMultiDrag(event)
}
if (event.action == MotionEvent.ACTION_UP) {
if (DEBUG) {
2020-11-04 18:07:40 +01:00
Log.d(
TAG,
"onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
" [${event.rawX}, ${event.rawY}]"
)
2020-10-21 16:40:22 +02:00
}
if (isMovingInPopup) {
isMovingInPopup = false
onScrollEnd(MainPlayer.PlayerType.POPUP, event)
}
if (isResizing) {
isResizing = false
initPointerDistance = (-1).toDouble()
initFirstPointerX = (-1).toFloat()
initFirstPointerY = (-1).toFloat()
initSecPointerX = (-1).toFloat()
initSecPointerY = (-1).toFloat()
onPopupResizingEnd()
2021-01-08 18:35:33 +01:00
player.changeState(player.currentState)
2020-10-21 16:40:22 +02:00
}
2021-01-08 18:35:33 +01:00
if (!player.isPopupClosing) {
savePopupPositionAndSizeToPrefs(player)
2020-10-21 16:40:22 +02:00
}
}
v.performClick()
return true
}
private fun handleMultiDrag(event: MotionEvent): Boolean {
if (initPointerDistance != -1.0 && event.pointerCount == 2) {
// get the movements of the fingers
2020-11-04 18:07:40 +01:00
val firstPointerMove = hypot(
event.getX(0) - initFirstPointerX.toDouble(),
event.getY(0) - initFirstPointerY.toDouble()
)
val secPointerMove = hypot(
event.getX(1) - initSecPointerX.toDouble(),
event.getY(1) - initSecPointerY.toDouble()
)
2020-10-21 16:40:22 +02:00
// minimum threshold beyond which pinch gesture will work
val minimumMove = ViewConfiguration.get(service).scaledTouchSlop
if (max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
2020-11-04 18:07:40 +01:00
val currentPointerDistance = hypot(
event.getX(0) - event.getX(1).toDouble(),
event.getY(0) - event.getY(1).toDouble()
)
2020-10-21 16:40:22 +02:00
2021-01-08 18:35:33 +01:00
val popupWidth = player.popupLayoutParams!!.width.toDouble()
2020-10-21 16:40:22 +02:00
// change co-ordinates of popup so the center stays at the same position
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
initPointerDistance = currentPointerDistance
2021-01-08 18:35:33 +01:00
player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt()
2020-10-21 16:40:22 +02:00
2021-01-08 18:35:33 +01:00
player.checkPopupPositionBounds()
player.updateScreenSize()
player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt())
2020-10-21 16:40:22 +02:00
return true
}
}
return false
}
// ///////////////////////////////////////////////////////////////////
// Simple gestures
// ///////////////////////////////////////////////////////////////////
override fun onDown(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDown called with e = [$e]")
if (isDoubleTapping && isDoubleTapEnabled) {
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
return true
}
2021-01-08 18:35:33 +01:00
return if (player.popupPlayerSelected())
2020-10-21 16:40:22 +02:00
onDownInPopup(e)
else
true
}
private fun onDownInPopup(e: MotionEvent): Boolean {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
2021-01-08 18:35:33 +01:00
player.updateScreenSize()
player.checkPopupPositionBounds()
player.popupLayoutParams?.let {
initialPopupX = it.x
initialPopupY = it.y
}
2020-10-21 16:40:22 +02:00
return super.onDown(e)
}
override fun onDoubleTap(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDoubleTap called with e = [$e]")
onDoubleTap(e, getDisplayPortion(e))
return true
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
2021-01-08 18:35:33 +01:00
if (player.popupPlayerSelected()) {
if (player.exoPlayerIsNull())
2020-10-21 16:40:22 +02:00
return false
onSingleTap(MainPlayer.PlayerType.POPUP)
return true
} else {
super.onSingleTapConfirmed(e)
2021-01-08 18:35:33 +01:00
if (player.currentState == Player.STATE_BLOCKED)
2020-10-21 16:40:22 +02:00
return true
onSingleTap(MainPlayer.PlayerType.VIDEO)
}
return true
}
override fun onLongPress(e: MotionEvent?) {
2021-01-08 18:35:33 +01:00
if (player.popupPlayerSelected()) {
player.updateScreenSize()
player.checkPopupPositionBounds()
player.changePopupSize(player.screenWidth.toInt())
2020-10-21 16:40:22 +02:00
}
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
2021-01-08 18:35:33 +01:00
return if (player.popupPlayerSelected()) {
2020-10-21 16:40:22 +02:00
onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY)
} else {
onScrollInMain(initialEvent, movingEvent, distanceX, distanceY)
}
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
2021-01-08 18:35:33 +01:00
return if (player.popupPlayerSelected()) {
2020-10-21 16:40:22 +02:00
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityY)
if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) {
2021-01-08 18:35:33 +01:00
player.popupLayoutParams!!.x = velocityX.toInt()
2020-10-21 16:40:22 +02:00
}
if (absVelocityY > tossFlingVelocity) {
2021-01-08 18:35:33 +01:00
player.popupLayoutParams!!.y = velocityY.toInt()
2020-10-21 16:40:22 +02:00
}
2021-01-08 18:35:33 +01:00
player.checkPopupPositionBounds()
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
2020-10-21 16:40:22 +02:00
return true
}
return false
} else {
true
}
}
private fun onScrollInMain(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
2021-01-08 18:35:33 +01:00
if (!player.isFullscreen) {
2020-10-21 16:40:22 +02:00
return false
}
val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service)
2020-11-04 18:07:40 +01:00
val isTouchingNavigationBar: Boolean =
2021-01-08 18:35:33 +01:00
initialEvent.y > (player.rootView.height - getNavigationBarHeight(service))
2020-10-21 16:40:22 +02:00
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false
}
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
2020-11-04 18:07:40 +01:00
if (
!isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
2021-01-08 18:35:33 +01:00
player.currentState == Player.STATE_COMPLETED
2020-11-04 18:07:40 +01:00
) {
2020-10-21 16:40:22 +02:00
return false
}
isMovingInMain = true
2020-11-04 18:07:40 +01:00
onScroll(
MainPlayer.PlayerType.VIDEO,
getDisplayHalfPortion(initialEvent),
initialEvent,
movingEvent,
distanceX,
distanceY
)
2020-10-21 16:40:22 +02:00
return true
}
private fun onScrollInPopup(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (isResizing) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
}
if (!isMovingInPopup) {
player.closeOverlayButton.animate(true, 200)
2020-10-21 16:40:22 +02:00
}
isMovingInPopup = true
val diffX: Float = (movingEvent.rawX - initialEvent.rawX)
var posX: Float = (initialPopupX + diffX)
val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
var posY: Float = (initialPopupY + diffY)
2021-01-08 18:35:33 +01:00
if (posX > player.screenWidth - player.popupLayoutParams!!.width) {
posX = (player.screenWidth - player.popupLayoutParams!!.width)
2020-10-21 16:40:22 +02:00
} else if (posX < 0) {
posX = 0f
}
2021-01-08 18:35:33 +01:00
if (posY > player.screenHeight - player.popupLayoutParams!!.height) {
posY = (player.screenHeight - player.popupLayoutParams!!.height)
2020-10-21 16:40:22 +02:00
} else if (posY < 0) {
posY = 0f
}
2021-01-08 18:35:33 +01:00
player.popupLayoutParams!!.x = posX.toInt()
player.popupLayoutParams!!.y = posY.toInt()
2020-10-21 16:40:22 +02:00
2020-11-04 18:07:40 +01:00
onScroll(
MainPlayer.PlayerType.POPUP,
getDisplayHalfPortion(initialEvent),
initialEvent,
movingEvent,
distanceX,
distanceY
)
2020-10-21 16:40:22 +02:00
2021-01-08 18:35:33 +01:00
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
2020-10-21 16:40:22 +02:00
return true
}
// ///////////////////////////////////////////////////////////////////
// Multi double tapping
// ///////////////////////////////////////////////////////////////////
var doubleTapControls: DoubleTapListener? = null
private set
private val isDoubleTapEnabled: Boolean
get() = doubleTapDelay > 0
var isDoubleTapping = false
private set
fun doubleTapControls(listener: DoubleTapListener) = apply {
doubleTapControls = listener
}
private var doubleTapDelay = DOUBLE_TAP_DELAY
private val doubleTapHandler: Handler = Handler()
private val doubleTapRunnable = Runnable {
if (DEBUG)
Log.d(TAG, "doubleTapRunnable called")
isDoubleTapping = false
doubleTapControls?.onDoubleTapFinished()
}
fun startMultiDoubleTap(e: MotionEvent) {
if (!isDoubleTapping) {
if (DEBUG)
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
keepInDoubleTapMode()
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
}
}
fun keepInDoubleTapMode() {
if (DEBUG)
Log.d(TAG, "keepInDoubleTapMode called")
isDoubleTapping = true
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
}
fun endMultiDoubleTap() {
if (DEBUG)
Log.d(TAG, "endMultiDoubleTap called")
isDoubleTapping = false
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapControls?.onDoubleTapFinished()
}
2020-10-21 16:40:22 +02:00
// ///////////////////////////////////////////////////////////////////
// Utils
// ///////////////////////////////////////////////////////////////////
private fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return if (player.playerType == MainPlayer.PlayerType.POPUP && player.popupLayoutParams != null) {
2020-10-21 16:40:22 +02:00
when {
2021-01-08 18:35:33 +01:00
e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT
e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
2020-10-21 16:40:22 +02:00
else -> DisplayPortion.MIDDLE
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
2021-01-08 18:35:33 +01:00
e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT
e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
2020-10-21 16:40:22 +02:00
else -> DisplayPortion.MIDDLE
}
}
}
// Currently needed for scrolling since there is no action more the middle portion
private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
2021-01-08 18:35:33 +01:00
return if (player.playerType == MainPlayer.PlayerType.POPUP) {
2020-10-21 16:40:22 +02:00
when {
2021-01-08 18:35:33 +01:00
e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF
2020-10-21 16:40:22 +02:00
else -> DisplayPortion.RIGHT_HALF
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
2021-01-08 18:35:33 +01:00
e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF
2020-10-21 16:40:22 +02:00
else -> DisplayPortion.RIGHT_HALF
}
}
}
private fun getNavigationBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
private fun getStatusBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("status_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
companion object {
private const val TAG = "BasePlayerGestListener"
2021-01-08 18:35:33 +01:00
private val DEBUG = Player.DEBUG
2020-10-21 16:40:22 +02:00
private const val DOUBLE_TAP_DELAY = 550L
2020-10-21 16:40:22 +02:00
private const val MOVEMENT_THRESHOLD = 40
}
}