LibreTube/app/src/main/java/com/github/libretube/util/PlayerGestureController.kt

176 lines
6.1 KiB
Kotlin
Raw Normal View History

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
import android.view.View
import androidx.activity.viewModels
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.interfaces.PlayerGestureOptions
import com.github.libretube.ui.models.PlayerViewModel
import kotlin.math.abs
class PlayerGestureController(activity: BaseActivity, private val listener: PlayerGestureOptions) :
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.
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()
private val handler: Handler = Handler(Looper.getMainLooper())
2022-11-28 11:56:12 +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
private var isFullscreen = false
private var isMoving = false
var isEnabled = true
// 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
init {
gestureDetector = GestureDetector(activity, GestureListener(), handler)
scaleGestureDetector = ScaleGestureDetector(activity, ScaleGestureListener(), handler)
playerViewModel.isFullscreen.observe(activity) {
isFullscreen = it
listener.onFullscreenChange(it)
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_UP && isMoving) {
isMoving = false
listener.onSwipeEnd()
}
// Event can be already consumed by some view which may lead to NPE.
try {
scaleGestureDetector.onTouchEvent(event)
gestureDetector.onTouchEvent(event)
} 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.
return isFullscreen
}
2022-11-26 15:40:18 +05:30
private inner class ScaleGestureListener : ScaleGestureDetector.OnScaleGestureListener {
var scaleFactor: Float = 1f
override fun onScale(detector: ScaleGestureDetector): Boolean {
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 {
scaleFactor < 0.8 -> listener.onMinimize()
scaleFactor > 1.2 -> listener.onZoom()
2022-11-26 15:40:18 +05:30
}
scaleFactor = 1f
}
}
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 {
// Initially assume this event is for click
wasClick = true
if (isMoving || scaleGestureDetector.isInProgress) return false
if (isEnabled && isSecondClick()) {
handler.removeCallbacks(runnable)
lastDoubleClick = elapsedTime
val eventPositionPercentageX = xPos / width
when {
eventPositionPercentageX < 0.4 -> listener.onDoubleTapLeftScreen()
eventPositionPercentageX > 0.6 -> listener.onDoubleTapRightScreen()
else -> listener.onDoubleTapCenterScreen()
}
} 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 {
if (!isEnabled || scaleGestureDetector.isInProgress) return false
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
wasClick = false
when {
width * 0.5 > e1.x -> listener.onSwipeLeftScreen(distanceY)
width * 0.5 < e1.x -> listener.onSwipeRightScreen(distanceY)
}
return true
}
private val runnable = Runnable {
// If the last event was for scroll or pinch then avoid single tap call
if (!wasClick || isSecondClick()) return@Runnable
listener.onSingleTap()
}
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
}
}