2022-11-25 16:26:45 +05:30
|
|
|
package com.github.libretube.util
|
|
|
|
|
|
|
|
import android.annotation.SuppressLint
|
|
|
|
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
|
2022-11-26 15:40:18 +05:30
|
|
|
import android.view.ScaleGestureDetector
|
2022-11-25 16:26:45 +05:30
|
|
|
import android.view.View
|
2022-11-28 11:54:31 +05:30
|
|
|
import androidx.activity.viewModels
|
|
|
|
import com.github.libretube.ui.base.BaseActivity
|
2022-11-25 16:26:45 +05:30
|
|
|
import com.github.libretube.ui.interfaces.PlayerGestureOptions
|
2022-11-28 11:54:31 +05:30
|
|
|
import com.github.libretube.ui.models.PlayerViewModel
|
2022-11-25 16:26:45 +05:30
|
|
|
import kotlin.math.abs
|
|
|
|
|
2022-11-28 11:54:31 +05:30
|
|
|
class PlayerGestureController(activity: BaseActivity, private val listener: PlayerGestureOptions) :
|
2022-11-25 16:26:45 +05:30
|
|
|
View.OnTouchListener {
|
|
|
|
|
2022-11-28 11:56:12 +05:30
|
|
|
// width and height should be obtained each time using getter to adopt layout
|
|
|
|
// size changes.
|
2022-11-25 16:26:45 +05:30
|
|
|
private val width get() = Resources.getSystem().displayMetrics.widthPixels
|
|
|
|
private val height get() = Resources.getSystem().displayMetrics.heightPixels
|
|
|
|
private val elapsedTime get() = SystemClock.elapsedRealtime()
|
|
|
|
|
2022-11-28 11:56:12 +05:30
|
|
|
private val playerViewModel: PlayerViewModel by activity.viewModels()
|
2022-11-25 16:26:45 +05:30
|
|
|
private val handler: Handler = Handler(Looper.getMainLooper())
|
2022-11-28 11:56:12 +05:30
|
|
|
|
2022-11-25 16:26:45 +05:30
|
|
|
private val gestureDetector: GestureDetector
|
2022-11-26 15:40:18 +05:30
|
|
|
private val scaleGestureDetector: ScaleGestureDetector
|
2022-11-28 11:56:12 +05:30
|
|
|
|
2022-11-28 11:54:31 +05:30
|
|
|
private var isFullscreen = false
|
2022-11-25 16:26:45 +05:30
|
|
|
private var isMoving = false
|
|
|
|
var isEnabled = true
|
|
|
|
|
2022-12-01 22:14:59 +05:30
|
|
|
// Indicates last touch event was for click or other gesture, used to avoid single click
|
|
|
|
// by runnable when scroll or pinch gesture already completed.
|
|
|
|
var wasClick = true
|
|
|
|
|
2022-11-25 16:26:45 +05:30
|
|
|
init {
|
2022-11-28 11:54:31 +05:30
|
|
|
gestureDetector = GestureDetector(activity, GestureListener(), handler)
|
|
|
|
scaleGestureDetector = ScaleGestureDetector(activity, ScaleGestureListener(), handler)
|
|
|
|
|
|
|
|
playerViewModel.isFullscreen.observe(activity) {
|
|
|
|
isFullscreen = it
|
|
|
|
listener.onFullscreenChange(it)
|
|
|
|
}
|
2022-11-25 16:26:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
|
|
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
|
|
|
if (event.action == MotionEvent.ACTION_UP && isMoving) {
|
|
|
|
isMoving = false
|
2022-11-26 19:40:39 +05:30
|
|
|
listener.onSwipeEnd()
|
2022-11-25 16:26:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
// Event can be already consumed by some view which may lead to NPE.
|
|
|
|
try {
|
2022-11-26 19:40:39 +05:30
|
|
|
scaleGestureDetector.onTouchEvent(event)
|
|
|
|
gestureDetector.onTouchEvent(event)
|
2022-11-25 16:26:45 +05:30
|
|
|
} catch (_: Exception) { }
|
|
|
|
|
2022-11-28 11:56:12 +05:30
|
|
|
// If video is playing in full-screen mode, then allow `onScroll` to consume
|
|
|
|
// event and return true.
|
2022-11-28 11:54:31 +05:30
|
|
|
return isFullscreen
|
2022-11-25 16:26:45 +05:30
|
|
|
}
|
|
|
|
|
2022-11-26 15:40:18 +05:30
|
|
|
private inner class ScaleGestureListener : ScaleGestureDetector.OnScaleGestureListener {
|
|
|
|
var scaleFactor: Float = 1f
|
|
|
|
|
|
|
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
2022-12-01 22:14:59 +05:30
|
|
|
wasClick = false
|
2022-11-26 15:40:18 +05:30
|
|
|
scaleFactor *= detector.scaleFactor
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onScaleEnd(detector: ScaleGestureDetector) {
|
|
|
|
when {
|
2022-11-26 19:40:39 +05:30
|
|
|
scaleFactor < 0.8 -> listener.onMinimize()
|
|
|
|
scaleFactor > 1.2 -> listener.onZoom()
|
2022-11-26 15:40:18 +05:30
|
|
|
}
|
|
|
|
scaleFactor = 1f
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-25 16:26:45 +05:30
|
|
|
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 {
|
2022-12-01 22:14:59 +05:30
|
|
|
// Initially assume this event is for click
|
|
|
|
wasClick = true
|
|
|
|
|
2022-11-26 19:40:39 +05:30
|
|
|
if (isMoving || scaleGestureDetector.isInProgress) return false
|
2022-11-25 16:26:45 +05:30
|
|
|
|
|
|
|
if (isEnabled && isSecondClick()) {
|
|
|
|
handler.removeCallbacks(runnable)
|
|
|
|
lastDoubleClick = elapsedTime
|
|
|
|
val eventPositionPercentageX = xPos / width
|
|
|
|
|
|
|
|
when {
|
2022-11-26 19:40:39 +05:30
|
|
|
eventPositionPercentageX < 0.4 -> listener.onDoubleTapLeftScreen()
|
|
|
|
eventPositionPercentageX > 0.6 -> listener.onDoubleTapRightScreen()
|
|
|
|
else -> listener.onDoubleTapCenterScreen()
|
2022-11-25 16:26:45 +05:30
|
|
|
}
|
|
|
|
} 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 {
|
2022-11-26 19:40:39 +05:30
|
|
|
if (!isEnabled || scaleGestureDetector.isInProgress) return false
|
2022-11-25 16:26:45 +05:30
|
|
|
|
|
|
|
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
|
2022-12-01 22:14:59 +05:30
|
|
|
wasClick = false
|
2022-11-25 16:26:45 +05:30
|
|
|
|
|
|
|
when {
|
2022-11-26 19:40:39 +05:30
|
|
|
width * 0.5 > e1.x -> listener.onSwipeLeftScreen(distanceY)
|
|
|
|
width * 0.5 < e1.x -> listener.onSwipeRightScreen(distanceY)
|
2022-11-25 16:26:45 +05:30
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
private val runnable = Runnable {
|
2022-12-01 22:14:59 +05:30
|
|
|
// If the last event was for scroll or pinch then avoid single tap call
|
|
|
|
if (!wasClick || isSecondClick()) return@Runnable
|
2022-11-26 19:40:39 +05:30
|
|
|
listener.onSingleTap()
|
2022-11-25 16:26:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|