mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-12-03 17:22:54 +01:00
feat: disable swipe-controls
when player controls are visible (#123)
This commit is contained in:
parent
2b763373ee
commit
6d8c0a0c73
@ -0,0 +1,84 @@
|
|||||||
|
package app.revanced.integrations.shared
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import app.revanced.integrations.utils.ReVancedUtils
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default implementation of [PlayerControlsVisibilityObserver]
|
||||||
|
*
|
||||||
|
* @param activity activity that contains the controls_layout view
|
||||||
|
*/
|
||||||
|
class PlayerControlsVisibilityObserverImpl(
|
||||||
|
private val activity: Activity
|
||||||
|
) : PlayerControlsVisibilityObserver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* id of the direct parent of controls_layout, R.id.youtube_controls_overlay
|
||||||
|
*/
|
||||||
|
private val controlsLayoutParentId =
|
||||||
|
ReVancedUtils.getResourceIdByName(activity, "id", "youtube_controls_overlay")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* id of R.id.controls_layout
|
||||||
|
*/
|
||||||
|
private val controlsLayoutId =
|
||||||
|
ReVancedUtils.getResourceIdByName(activity, "id", "controls_layout")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reference to the controls layout view
|
||||||
|
*/
|
||||||
|
private var controlsLayoutView = WeakReference<View>(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is the [controlsLayoutView] set to a valid reference of a view?
|
||||||
|
*/
|
||||||
|
private val isAttached: Boolean
|
||||||
|
get() {
|
||||||
|
val view = controlsLayoutView.get()
|
||||||
|
return view != null && view.parent != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find and attach the controls_layout view if needed
|
||||||
|
*/
|
||||||
|
private fun maybeAttach() {
|
||||||
|
if (isAttached) return
|
||||||
|
|
||||||
|
// find parent, then controls_layout view
|
||||||
|
// this is needed because there may be two views where id=R.id.controls_layout
|
||||||
|
// because why should google confine themselves to their own guidelines...
|
||||||
|
activity.findViewById<ViewGroup>(controlsLayoutParentId)?.let { parent ->
|
||||||
|
parent.findViewById<View>(controlsLayoutId)?.let {
|
||||||
|
controlsLayoutView = WeakReference(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val playerControlsVisibility: Int
|
||||||
|
get() {
|
||||||
|
maybeAttach()
|
||||||
|
return controlsLayoutView.get()?.visibility ?: View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override val arePlayerControlsVisible: Boolean
|
||||||
|
get() = playerControlsVisibility == View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* provides the visibility status of the fullscreen player controls_layout view.
|
||||||
|
* this can be used for detecting when the player controls are shown
|
||||||
|
*/
|
||||||
|
interface PlayerControlsVisibilityObserver {
|
||||||
|
/**
|
||||||
|
* current visibility int of the controls_layout view
|
||||||
|
*/
|
||||||
|
val playerControlsVisibility: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is the value of [playerControlsVisibility] equal to [View.VISIBLE]?
|
||||||
|
*/
|
||||||
|
val arePlayerControlsVisible: Boolean
|
||||||
|
}
|
@ -10,8 +10,9 @@ import app.revanced.integrations.swipecontrols.controller.AudioVolumeController
|
|||||||
import app.revanced.integrations.swipecontrols.controller.ScreenBrightnessController
|
import app.revanced.integrations.swipecontrols.controller.ScreenBrightnessController
|
||||||
import app.revanced.integrations.swipecontrols.controller.SwipeZonesController
|
import app.revanced.integrations.swipecontrols.controller.SwipeZonesController
|
||||||
import app.revanced.integrations.swipecontrols.controller.VolumeKeysController
|
import app.revanced.integrations.swipecontrols.controller.VolumeKeysController
|
||||||
import app.revanced.integrations.swipecontrols.controller.gesture.NoPtSSwipeGestureController
|
import app.revanced.integrations.swipecontrols.controller.gesture.ClassicSwipeController
|
||||||
import app.revanced.integrations.swipecontrols.controller.gesture.SwipeGestureController
|
import app.revanced.integrations.swipecontrols.controller.gesture.PressToSwipeController
|
||||||
|
import app.revanced.integrations.swipecontrols.controller.gesture.core.GestureController
|
||||||
import app.revanced.integrations.swipecontrols.misc.Rectangle
|
import app.revanced.integrations.swipecontrols.misc.Rectangle
|
||||||
import app.revanced.integrations.swipecontrols.views.SwipeControlsOverlayLayout
|
import app.revanced.integrations.swipecontrols.views.SwipeControlsOverlayLayout
|
||||||
import app.revanced.integrations.utils.LogHelper
|
import app.revanced.integrations.utils.LogHelper
|
||||||
@ -52,7 +53,7 @@ class SwipeControlsHostActivity : Activity() {
|
|||||||
/**
|
/**
|
||||||
* main gesture controller
|
* main gesture controller
|
||||||
*/
|
*/
|
||||||
private lateinit var gesture: SwipeGestureController
|
private lateinit var gesture: GestureController
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* main volume keys controller
|
* main volume keys controller
|
||||||
@ -71,13 +72,12 @@ class SwipeControlsHostActivity : Activity() {
|
|||||||
// create controllers
|
// create controllers
|
||||||
LogHelper.info(this.javaClass, "initializing swipe controls controllers")
|
LogHelper.info(this.javaClass, "initializing swipe controls controllers")
|
||||||
config = SwipeControlsConfigurationProvider(this)
|
config = SwipeControlsConfigurationProvider(this)
|
||||||
gesture = createGestureController()
|
|
||||||
keys = VolumeKeysController(this)
|
keys = VolumeKeysController(this)
|
||||||
audio = createAudioController()
|
audio = createAudioController()
|
||||||
screen = createScreenController()
|
screen = createScreenController()
|
||||||
|
|
||||||
// create overlay
|
// create overlay
|
||||||
SwipeControlsOverlayLayout(this).let {
|
SwipeControlsOverlayLayout(this, config).let {
|
||||||
overlay = it
|
overlay = it
|
||||||
contentRoot.addView(it)
|
contentRoot.addView(it)
|
||||||
}
|
}
|
||||||
@ -92,6 +92,9 @@ class SwipeControlsHostActivity : Activity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create the gesture controller
|
||||||
|
gesture = createGestureController()
|
||||||
|
|
||||||
// listen for changes in the player type
|
// listen for changes in the player type
|
||||||
PlayerType.onChange += this::onPlayerTypeChanged
|
PlayerType.onChange += this::onPlayerTypeChanged
|
||||||
|
|
||||||
@ -109,13 +112,13 @@ class SwipeControlsHostActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||||
return if ((ev != null) && gesture.onTouchEvent(ev)) true else {
|
return if ((ev != null) && gesture.submitTouchEvent(ev)) true else {
|
||||||
super.dispatchTouchEvent(ev)
|
super.dispatchTouchEvent(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispatchKeyEvent(ev: KeyEvent?): Boolean {
|
override fun dispatchKeyEvent(ev: KeyEvent?): Boolean {
|
||||||
return if((ev != null) && keys.onKeyEvent(ev)) true else {
|
return if ((ev != null) && keys.onKeyEvent(ev)) true else {
|
||||||
super.dispatchKeyEvent(ev)
|
super.dispatchKeyEvent(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,8 +166,8 @@ class SwipeControlsHostActivity : Activity() {
|
|||||||
*/
|
*/
|
||||||
private fun createGestureController() =
|
private fun createGestureController() =
|
||||||
if (config.shouldEnablePressToSwipe)
|
if (config.shouldEnablePressToSwipe)
|
||||||
SwipeGestureController(this)
|
PressToSwipeController(this)
|
||||||
else NoPtSSwipeGestureController(this)
|
else ClassicSwipeController(this)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,111 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.controller.gesture
|
||||||
|
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import app.revanced.integrations.shared.PlayerControlsVisibilityObserver
|
||||||
|
import app.revanced.integrations.shared.PlayerControlsVisibilityObserverImpl
|
||||||
|
import app.revanced.integrations.swipecontrols.SwipeControlsHostActivity
|
||||||
|
import app.revanced.integrations.swipecontrols.controller.gesture.core.BaseGestureController
|
||||||
|
import app.revanced.integrations.swipecontrols.controller.gesture.core.SwipeDetector
|
||||||
|
import app.revanced.integrations.swipecontrols.misc.contains
|
||||||
|
import app.revanced.integrations.swipecontrols.misc.toPoint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* provides the classic swipe controls experience, as it was with 'XFenster'
|
||||||
|
*
|
||||||
|
* @param controller reference to the main swipe controller
|
||||||
|
*/
|
||||||
|
class ClassicSwipeController(
|
||||||
|
private val controller: SwipeControlsHostActivity
|
||||||
|
) : BaseGestureController(controller),
|
||||||
|
PlayerControlsVisibilityObserver by PlayerControlsVisibilityObserverImpl(controller) {
|
||||||
|
/**
|
||||||
|
* the last event captured in [onDown]
|
||||||
|
*/
|
||||||
|
private var lastOnDownEvent: MotionEvent? = null
|
||||||
|
|
||||||
|
override val shouldForceInterceptEvents: Boolean
|
||||||
|
get() = currentSwipe == SwipeDetector.SwipeDirection.VERTICAL
|
||||||
|
|
||||||
|
override fun isInSwipeZone(motionEvent: MotionEvent): Boolean {
|
||||||
|
val inVolumeZone = if (controller.config.enableVolumeControls)
|
||||||
|
(motionEvent.toPoint() in controller.zones.volume) else false
|
||||||
|
val inBrightnessZone = if (controller.config.enableBrightnessControl)
|
||||||
|
(motionEvent.toPoint() in controller.zones.brightness) else false
|
||||||
|
|
||||||
|
return inVolumeZone || inBrightnessZone
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldDropMotion(motionEvent: MotionEvent): Boolean {
|
||||||
|
// ignore gestures with more than one pointer
|
||||||
|
// when such a gesture is detected, dispatch the first event of the gesture to downstream
|
||||||
|
if (motionEvent.pointerCount > 1) {
|
||||||
|
lastOnDownEvent?.let {
|
||||||
|
controller.dispatchDownstreamTouchEvent(it)
|
||||||
|
it.recycle()
|
||||||
|
}
|
||||||
|
lastOnDownEvent = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore gestures when player controls are visible
|
||||||
|
return arePlayerControlsVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDown(motionEvent: MotionEvent): Boolean {
|
||||||
|
// save the event for later
|
||||||
|
lastOnDownEvent?.recycle()
|
||||||
|
lastOnDownEvent = MotionEvent.obtain(motionEvent)
|
||||||
|
|
||||||
|
// must be inside swipe zone
|
||||||
|
return isInSwipeZone(motionEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSingleTapUp(motionEvent: MotionEvent): Boolean {
|
||||||
|
MotionEvent.obtain(motionEvent).let {
|
||||||
|
it.action = MotionEvent.ACTION_DOWN
|
||||||
|
controller.dispatchDownstreamTouchEvent(it)
|
||||||
|
it.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDoubleTapEvent(motionEvent: MotionEvent?): Boolean {
|
||||||
|
MotionEvent.obtain(motionEvent).let {
|
||||||
|
controller.dispatchDownstreamTouchEvent(it)
|
||||||
|
it.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onDoubleTapEvent(motionEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLongPress(motionEvent: MotionEvent?) {
|
||||||
|
MotionEvent.obtain(motionEvent).let {
|
||||||
|
controller.dispatchDownstreamTouchEvent(it)
|
||||||
|
it.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onLongPress(motionEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwipe(
|
||||||
|
from: MotionEvent,
|
||||||
|
to: MotionEvent,
|
||||||
|
distanceX: Double,
|
||||||
|
distanceY: Double
|
||||||
|
): Boolean {
|
||||||
|
// cancel if not vertical
|
||||||
|
if (currentSwipe != SwipeDetector.SwipeDirection.VERTICAL) return false
|
||||||
|
return when (from.toPoint()) {
|
||||||
|
in controller.zones.volume -> {
|
||||||
|
scrollVolume(distanceY)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
in controller.zones.brightness -> {
|
||||||
|
scrollBrightness(distanceY)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
package app.revanced.integrations.swipecontrols.controller.gesture
|
|
||||||
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import app.revanced.integrations.swipecontrols.SwipeControlsHostActivity
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [SwipeGestureController], but with press-to-swipe disabled because a lot of people dislike the feature.
|
|
||||||
* If you want to change something, try to do it in [SwipeGestureController] so that both configurations can benefit from it
|
|
||||||
*/
|
|
||||||
class NoPtSSwipeGestureController(controller: SwipeControlsHostActivity) :
|
|
||||||
SwipeGestureController(controller) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* to disable press-to-swipe, we have to become press-to-swipe
|
|
||||||
*/
|
|
||||||
override var inSwipeSession
|
|
||||||
get() = true
|
|
||||||
set(_) {}
|
|
||||||
|
|
||||||
override fun onLongPress(e: MotionEvent?) {
|
|
||||||
if (e == null) return
|
|
||||||
|
|
||||||
// send GestureDetector a ACTION_CANCEL event so it will handle further events
|
|
||||||
// if this is left out, swipe-to-dismiss is triggered when scrolling down
|
|
||||||
e.action = MotionEvent.ACTION_CANCEL
|
|
||||||
detector.onTouchEvent(e)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,72 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.controller.gesture
|
||||||
|
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import app.revanced.integrations.swipecontrols.SwipeControlsHostActivity
|
||||||
|
import app.revanced.integrations.swipecontrols.controller.gesture.core.BaseGestureController
|
||||||
|
import app.revanced.integrations.swipecontrols.controller.gesture.core.SwipeDetector
|
||||||
|
import app.revanced.integrations.swipecontrols.misc.contains
|
||||||
|
import app.revanced.integrations.swipecontrols.misc.toPoint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* provides the press-to-swipe (PtS) swipe controls experience
|
||||||
|
*
|
||||||
|
* @param controller reference to the main swipe controller
|
||||||
|
*/
|
||||||
|
class PressToSwipeController(
|
||||||
|
private val controller: SwipeControlsHostActivity
|
||||||
|
) : BaseGestureController(controller) {
|
||||||
|
/**
|
||||||
|
* monitors if the user is currently in a swipe session.
|
||||||
|
*/
|
||||||
|
private var isInSwipeSession = false
|
||||||
|
|
||||||
|
override val shouldForceInterceptEvents: Boolean
|
||||||
|
get() = currentSwipe == SwipeDetector.SwipeDirection.VERTICAL && isInSwipeSession
|
||||||
|
|
||||||
|
override fun shouldDropMotion(motionEvent: MotionEvent): Boolean = false
|
||||||
|
|
||||||
|
override fun isInSwipeZone(motionEvent: MotionEvent): Boolean {
|
||||||
|
val inVolumeZone = if (controller.config.enableVolumeControls)
|
||||||
|
(motionEvent.toPoint() in controller.zones.volume) else false
|
||||||
|
val inBrightnessZone = if (controller.config.enableBrightnessControl)
|
||||||
|
(motionEvent.toPoint() in controller.zones.brightness) else false
|
||||||
|
|
||||||
|
return inVolumeZone || inBrightnessZone
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUp(motionEvent: MotionEvent) {
|
||||||
|
super.onUp(motionEvent)
|
||||||
|
isInSwipeSession = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLongPress(motionEvent: MotionEvent) {
|
||||||
|
// enter swipe session with feedback
|
||||||
|
isInSwipeSession = true
|
||||||
|
controller.overlay.onEnterSwipeSession()
|
||||||
|
|
||||||
|
// send GestureDetector a ACTION_CANCEL event so it will handle further events
|
||||||
|
motionEvent.action = MotionEvent.ACTION_CANCEL
|
||||||
|
detector.onTouchEvent(motionEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwipe(
|
||||||
|
from: MotionEvent,
|
||||||
|
to: MotionEvent,
|
||||||
|
distanceX: Double,
|
||||||
|
distanceY: Double
|
||||||
|
): Boolean {
|
||||||
|
// cancel if not in swipe session or vertical
|
||||||
|
if (!isInSwipeSession || currentSwipe != SwipeDetector.SwipeDirection.VERTICAL) return false
|
||||||
|
return when (from.toPoint()) {
|
||||||
|
in controller.zones.volume -> {
|
||||||
|
scrollVolume(distanceY)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
in controller.zones.brightness -> {
|
||||||
|
scrollBrightness(distanceY)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,232 +0,0 @@
|
|||||||
package app.revanced.integrations.swipecontrols.controller.gesture
|
|
||||||
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.GestureDetector
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import app.revanced.integrations.swipecontrols.SwipeControlsHostActivity
|
|
||||||
import app.revanced.integrations.swipecontrols.misc.ScrollDistanceHelper
|
|
||||||
import app.revanced.integrations.swipecontrols.misc.applyDimension
|
|
||||||
import app.revanced.integrations.swipecontrols.misc.contains
|
|
||||||
import app.revanced.integrations.swipecontrols.misc.toPoint
|
|
||||||
import app.revanced.integrations.utils.LogHelper
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* base gesture controller for volume and brightness swipe controls controls, with press-to-swipe enabled
|
|
||||||
* for the controller without press-to-swipe, see [NoPtSSwipeGestureController]
|
|
||||||
*
|
|
||||||
* @param controller reference to main controller instance
|
|
||||||
*/
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
open class SwipeGestureController(
|
|
||||||
private val controller: SwipeControlsHostActivity
|
|
||||||
) :
|
|
||||||
GestureDetector.SimpleOnGestureListener() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the main gesture detector that powers everything
|
|
||||||
*/
|
|
||||||
protected open val detector = GestureDetector(controller, this)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* to enable swipe controls, users must first long- press. this flags monitors that long- press
|
|
||||||
* NOTE: if you dislike press-to-swipe, and want it disabled, have a look at [NoPtSSwipeGestureController]. it does exactly that
|
|
||||||
*/
|
|
||||||
protected open var inSwipeSession = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* currently in- progress swipe
|
|
||||||
*/
|
|
||||||
protected open var currentSwipe: SwipeDirection = SwipeDirection.NONE
|
|
||||||
|
|
||||||
/**
|
|
||||||
* were downstream event cancelled already? used by [onScroll]
|
|
||||||
*/
|
|
||||||
protected open var didCancelDownstream = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* should [onTouchEvent] force- intercept all touch events?
|
|
||||||
*/
|
|
||||||
protected open val shouldForceInterceptEvents: Boolean
|
|
||||||
get() = currentSwipe == SwipeDirection.VERTICAL && inSwipeSession
|
|
||||||
|
|
||||||
/**
|
|
||||||
* scroller for volume adjustment
|
|
||||||
*/
|
|
||||||
protected open val volumeScroller = ScrollDistanceHelper(
|
|
||||||
10.applyDimension(
|
|
||||||
controller,
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP
|
|
||||||
)
|
|
||||||
) { _, _, direction ->
|
|
||||||
controller.audio?.apply {
|
|
||||||
volume += direction
|
|
||||||
controller.overlay.onVolumeChanged(volume, maxVolume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* scroller for screen brightness adjustment
|
|
||||||
*/
|
|
||||||
protected open val brightnessScroller = ScrollDistanceHelper(
|
|
||||||
1.applyDimension(
|
|
||||||
controller,
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP
|
|
||||||
)
|
|
||||||
) { _, _, direction ->
|
|
||||||
controller.screen?.apply {
|
|
||||||
if (screenBrightness > 0 || direction > 0) {
|
|
||||||
screenBrightness += direction
|
|
||||||
} else {
|
|
||||||
restoreDefaultBrightness()
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.overlay.onBrightnessChanged(screenBrightness)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* touch event callback
|
|
||||||
*
|
|
||||||
* @param motionEvent the motion event that was received
|
|
||||||
* @return intercept the event? if true, child views will not receive the event
|
|
||||||
*/
|
|
||||||
fun onTouchEvent(motionEvent: MotionEvent): Boolean {
|
|
||||||
if (!controller.config.enableSwipeControls) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (motionEvent.action == MotionEvent.ACTION_UP) {
|
|
||||||
onUp(motionEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (shouldForceInterceptEvents || inSwipeZone(motionEvent)) {
|
|
||||||
detector.onTouchEvent(motionEvent) or shouldForceInterceptEvents
|
|
||||||
} else false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if provided motion event is in any active swipe zone?
|
|
||||||
*
|
|
||||||
* @param e the event to check
|
|
||||||
* @return is the event in any active swipe zone?
|
|
||||||
*/
|
|
||||||
open fun inSwipeZone(e: MotionEvent): Boolean {
|
|
||||||
val inVolumeZone = if (controller.config.enableVolumeControls)
|
|
||||||
(e.toPoint() in controller.zones.volume) else false
|
|
||||||
val inBrightnessZone = if (controller.config.enableBrightnessControl)
|
|
||||||
(e.toPoint() in controller.zones.brightness) else false
|
|
||||||
|
|
||||||
return inVolumeZone || inBrightnessZone
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* custom handler for ACTION_UP event, because GestureDetector doesn't offer that :|
|
|
||||||
* basically just resets all flags to non- swiping values
|
|
||||||
*
|
|
||||||
* @param e the motion event
|
|
||||||
*/
|
|
||||||
open fun onUp(e: MotionEvent) {
|
|
||||||
LogHelper.debug(this.javaClass, "onUp(${e.x}, ${e.y}, ${e.action})")
|
|
||||||
inSwipeSession = false
|
|
||||||
currentSwipe = SwipeDirection.NONE
|
|
||||||
didCancelDownstream = false
|
|
||||||
volumeScroller.reset()
|
|
||||||
brightnessScroller.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongPress(e: MotionEvent?) {
|
|
||||||
if (e == null) return
|
|
||||||
LogHelper.debug(this.javaClass, "onLongPress(${e.x}, ${e.y}, ${e.action})")
|
|
||||||
|
|
||||||
// enter swipe session with feedback
|
|
||||||
inSwipeSession = true
|
|
||||||
controller.overlay.onEnterSwipeSession()
|
|
||||||
|
|
||||||
// send GestureDetector a ACTION_CANCEL event so it will handle further events
|
|
||||||
e.action = MotionEvent.ACTION_CANCEL
|
|
||||||
detector.onTouchEvent(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScroll(
|
|
||||||
eFrom: MotionEvent?,
|
|
||||||
eTo: MotionEvent?,
|
|
||||||
disX: Float,
|
|
||||||
disY: Float
|
|
||||||
): Boolean {
|
|
||||||
if (eFrom == null || eTo == null) return false
|
|
||||||
LogHelper.debug(
|
|
||||||
this.javaClass,
|
|
||||||
"onScroll(from: [${eFrom.x}, ${eFrom.y}, ${eFrom.action}], to: [${eTo.x}, ${eTo.y}, ${eTo.action}], d: [$disX, $disY])"
|
|
||||||
)
|
|
||||||
|
|
||||||
return when (currentSwipe) {
|
|
||||||
// no swipe direction was detected yet, try to detect one
|
|
||||||
// if the user did not swipe far enough, we cannot detect what direction they swiped
|
|
||||||
// so we wait until a greater distance was swiped
|
|
||||||
// NOTE: sqrt() can be high- cost, so using squared magnitudes here
|
|
||||||
SwipeDirection.NONE -> {
|
|
||||||
val deltaX = abs(eTo.x - eFrom.x)
|
|
||||||
val deltaY = abs(eTo.y - eFrom.y)
|
|
||||||
val swipeMagnitudeSquared = deltaX.pow(2) + deltaY.pow(2)
|
|
||||||
if (swipeMagnitudeSquared > controller.config.swipeMagnitudeThreshold.pow(2)) {
|
|
||||||
currentSwipe = if (deltaY > deltaX) {
|
|
||||||
SwipeDirection.VERTICAL
|
|
||||||
} else {
|
|
||||||
SwipeDirection.HORIZONTAL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// horizontal swipe, we should leave this one for downstream to handle
|
|
||||||
SwipeDirection.HORIZONTAL -> false
|
|
||||||
|
|
||||||
// vertical swipe, could be for us
|
|
||||||
SwipeDirection.VERTICAL -> {
|
|
||||||
if (!inSwipeSession) {
|
|
||||||
// not in swipe session, let downstream handle this one
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// vertical & in swipe session, handle this one:
|
|
||||||
// first, send ACTION_CANCEL to downstream to let them known they should stop tracking events
|
|
||||||
if (!didCancelDownstream) {
|
|
||||||
val eCancel = MotionEvent.obtain(eFrom)
|
|
||||||
eCancel.action = MotionEvent.ACTION_CANCEL
|
|
||||||
controller.dispatchDownstreamTouchEvent(eCancel)
|
|
||||||
eCancel.recycle()
|
|
||||||
didCancelDownstream = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// then, process the event
|
|
||||||
when (eFrom.toPoint()) {
|
|
||||||
in controller.zones.volume -> volumeScroller.add(disY.toDouble())
|
|
||||||
in controller.zones.brightness -> brightnessScroller.add(disY.toDouble())
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* direction of a swipe
|
|
||||||
*/
|
|
||||||
enum class SwipeDirection {
|
|
||||||
/**
|
|
||||||
* swipe has no direction or no swipe
|
|
||||||
*/
|
|
||||||
NONE,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* swipe along the X- Axes
|
|
||||||
*/
|
|
||||||
HORIZONTAL,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* swipe along the Y- Axes
|
|
||||||
*/
|
|
||||||
VERTICAL
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,154 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.controller.gesture.core
|
||||||
|
|
||||||
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import app.revanced.integrations.swipecontrols.SwipeControlsHostActivity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the common base of all [GestureController] classes.
|
||||||
|
* handles most of the boilerplate code needed for gesture detection
|
||||||
|
*
|
||||||
|
* @param controller reference to the main swipe controller
|
||||||
|
*/
|
||||||
|
abstract class BaseGestureController(
|
||||||
|
private val controller: SwipeControlsHostActivity
|
||||||
|
) : GestureController,
|
||||||
|
GestureDetector.SimpleOnGestureListener(),
|
||||||
|
SwipeDetector by SwipeDetectorImpl(
|
||||||
|
controller.config.swipeMagnitudeThreshold.toDouble()
|
||||||
|
),
|
||||||
|
VolumeAndBrightnessScroller by VolumeAndBrightnessScrollerImpl(
|
||||||
|
controller,
|
||||||
|
controller.audio,
|
||||||
|
controller.screen,
|
||||||
|
controller.overlay,
|
||||||
|
10,
|
||||||
|
1
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the main gesture detector that powers everything
|
||||||
|
*/
|
||||||
|
@Suppress("LeakingThis")
|
||||||
|
protected val detector = GestureDetector(controller, this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* were downstream event cancelled already? used in [onScroll]
|
||||||
|
*/
|
||||||
|
private var didCancelDownstream = false
|
||||||
|
|
||||||
|
override fun submitTouchEvent(motionEvent: MotionEvent): Boolean {
|
||||||
|
// ignore if swipe is disabled
|
||||||
|
if (!controller.config.enableSwipeControls) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a copy of the event so we can modify it
|
||||||
|
// without causing any issues downstream
|
||||||
|
val me = MotionEvent.obtain(motionEvent)
|
||||||
|
|
||||||
|
// check if we should drop this motion
|
||||||
|
val dropped = shouldDropMotion(me)
|
||||||
|
if (dropped) {
|
||||||
|
me.action = MotionEvent.ACTION_CANCEL
|
||||||
|
}
|
||||||
|
|
||||||
|
// send the event to the detector
|
||||||
|
// if we force intercept events, the event is always consumed
|
||||||
|
val consumed = detector.onTouchEvent(me) || shouldForceInterceptEvents
|
||||||
|
|
||||||
|
// invoke the custom onUp handler
|
||||||
|
if (me.action == MotionEvent.ACTION_UP || me.action == MotionEvent.ACTION_CANCEL) {
|
||||||
|
onUp(me)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recycle the copy
|
||||||
|
me.recycle()
|
||||||
|
|
||||||
|
// do not consume dropped events
|
||||||
|
// or events outside of any swipe zone
|
||||||
|
return !dropped && consumed && isInSwipeZone(me)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* custom handler for [MotionEvent.ACTION_UP] event, because GestureDetector doesn't offer that :|
|
||||||
|
*
|
||||||
|
* @param motionEvent the motion event
|
||||||
|
*/
|
||||||
|
open fun onUp(motionEvent: MotionEvent) {
|
||||||
|
didCancelDownstream = false
|
||||||
|
resetSwipe()
|
||||||
|
resetScroller()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScroll(
|
||||||
|
from: MotionEvent,
|
||||||
|
to: MotionEvent,
|
||||||
|
distanceX: Float,
|
||||||
|
distanceY: Float
|
||||||
|
): Boolean {
|
||||||
|
// submit to swipe detector
|
||||||
|
submitForSwipe(from, to, distanceX, distanceY)
|
||||||
|
|
||||||
|
// call swipe callback if in a swipe
|
||||||
|
return if (currentSwipe != SwipeDetector.SwipeDirection.NONE) {
|
||||||
|
val consumed = onSwipe(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
distanceX.toDouble(),
|
||||||
|
distanceY.toDouble()
|
||||||
|
)
|
||||||
|
|
||||||
|
// if the swipe was consumed, cancel downstream events once
|
||||||
|
if (consumed && !didCancelDownstream) {
|
||||||
|
didCancelDownstream = true
|
||||||
|
MotionEvent.obtain(from).let {
|
||||||
|
it.action = MotionEvent.ACTION_CANCEL
|
||||||
|
controller.dispatchDownstreamTouchEvent(it)
|
||||||
|
it.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
consumed
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* should [submitTouchEvent] force- intercept all touch events?
|
||||||
|
*/
|
||||||
|
abstract val shouldForceInterceptEvents: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if provided motion event is in any active swipe zone?
|
||||||
|
*
|
||||||
|
* @param motionEvent the event to check
|
||||||
|
* @return is the event in any active swipe zone?
|
||||||
|
*/
|
||||||
|
abstract fun isInSwipeZone(motionEvent: MotionEvent): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if a touch event should be dropped.
|
||||||
|
* when a event is dropped, the gesture detector received a [MotionEvent.ACTION_CANCEL] event and the event is not consumed
|
||||||
|
*
|
||||||
|
* @param motionEvent the event to check
|
||||||
|
* @return should the event be dropped?
|
||||||
|
*/
|
||||||
|
abstract fun shouldDropMotion(motionEvent: MotionEvent): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handler for swipe events, once a swipe is detected.
|
||||||
|
* the direction of the swipe can be accessed in [currentSwipe]
|
||||||
|
*
|
||||||
|
* @param from start event of the swipe
|
||||||
|
* @param to end event of the swipe
|
||||||
|
* @param distanceX the horizontal distance of the swipe
|
||||||
|
* @param distanceY the vertical distance of the swipe
|
||||||
|
* @return was the event consumed?
|
||||||
|
*/
|
||||||
|
abstract fun onSwipe(
|
||||||
|
from: MotionEvent,
|
||||||
|
to: MotionEvent,
|
||||||
|
distanceX: Double,
|
||||||
|
distanceY: Double
|
||||||
|
): Boolean
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.controller.gesture.core
|
||||||
|
|
||||||
|
import android.view.MotionEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* describes a class that accepts motion events and detects gestures
|
||||||
|
*/
|
||||||
|
interface GestureController {
|
||||||
|
/**
|
||||||
|
* accept a touch event and try to detect the desired gestures using it
|
||||||
|
*
|
||||||
|
* @param motionEvent the motion event that was submitted
|
||||||
|
* @return was a gesture detected?
|
||||||
|
*/
|
||||||
|
fun submitTouchEvent(motionEvent: MotionEvent): Boolean
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.controller.gesture.core
|
||||||
|
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* describes a class that can detect swipes and their directionality
|
||||||
|
*/
|
||||||
|
interface SwipeDetector {
|
||||||
|
/**
|
||||||
|
* the currently detected swipe
|
||||||
|
*/
|
||||||
|
val currentSwipe: SwipeDirection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* submit a onScroll event for swipe detection
|
||||||
|
*
|
||||||
|
* @param from start event
|
||||||
|
* @param to end event
|
||||||
|
* @param distanceX horizontal scroll distance
|
||||||
|
* @param distanceY vertical scroll distance
|
||||||
|
*/
|
||||||
|
fun submitForSwipe(
|
||||||
|
from: MotionEvent,
|
||||||
|
to: MotionEvent,
|
||||||
|
distanceX: Float,
|
||||||
|
distanceY: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reset the swipe detection
|
||||||
|
*/
|
||||||
|
fun resetSwipe()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* direction of a swipe
|
||||||
|
*/
|
||||||
|
enum class SwipeDirection {
|
||||||
|
/**
|
||||||
|
* swipe has no direction or no swipe
|
||||||
|
*/
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* swipe along the X- Axes
|
||||||
|
*/
|
||||||
|
HORIZONTAL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* swipe along the Y- Axes
|
||||||
|
*/
|
||||||
|
VERTICAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* detector that can detect swipes and their directionality
|
||||||
|
*
|
||||||
|
* @param swipeMagnitudeThreshold minimum magnitude before a swipe is detected as such
|
||||||
|
*/
|
||||||
|
class SwipeDetectorImpl(
|
||||||
|
private val swipeMagnitudeThreshold: Double
|
||||||
|
) : SwipeDetector {
|
||||||
|
override var currentSwipe = SwipeDetector.SwipeDirection.NONE
|
||||||
|
|
||||||
|
override fun submitForSwipe(
|
||||||
|
from: MotionEvent,
|
||||||
|
to: MotionEvent,
|
||||||
|
distanceX: Float,
|
||||||
|
distanceY: Float
|
||||||
|
) {
|
||||||
|
if (currentSwipe == SwipeDetector.SwipeDirection.NONE) {
|
||||||
|
// no swipe direction was detected yet, try to detect one
|
||||||
|
// if the user did not swipe far enough, we cannot detect what direction they swiped
|
||||||
|
// so we wait until a greater distance was swiped
|
||||||
|
// NOTE: sqrt() can be high- cost, so using squared magnitudes here
|
||||||
|
val deltaX = abs(to.x - from.x)
|
||||||
|
val deltaY = abs(to.y - from.y)
|
||||||
|
val swipeMagnitudeSquared = deltaX.pow(2) + deltaY.pow(2)
|
||||||
|
if (swipeMagnitudeSquared > swipeMagnitudeThreshold.pow(2)) {
|
||||||
|
currentSwipe = if (deltaY > deltaX) {
|
||||||
|
SwipeDetector.SwipeDirection.VERTICAL
|
||||||
|
} else {
|
||||||
|
SwipeDetector.SwipeDirection.HORIZONTAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetSwipe() {
|
||||||
|
currentSwipe = SwipeDetector.SwipeDirection.NONE
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.controller.gesture.core
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.TypedValue
|
||||||
|
import app.revanced.integrations.swipecontrols.controller.AudioVolumeController
|
||||||
|
import app.revanced.integrations.swipecontrols.controller.ScreenBrightnessController
|
||||||
|
import app.revanced.integrations.swipecontrols.misc.ScrollDistanceHelper
|
||||||
|
import app.revanced.integrations.swipecontrols.misc.SwipeControlsOverlay
|
||||||
|
import app.revanced.integrations.swipecontrols.misc.applyDimension
|
||||||
|
|
||||||
|
/**
|
||||||
|
* describes a class that controls volume and brightness based on scrolling events
|
||||||
|
*/
|
||||||
|
interface VolumeAndBrightnessScroller {
|
||||||
|
/**
|
||||||
|
* submit a scroll for volume adjustment
|
||||||
|
*
|
||||||
|
* @param distance the scroll distance
|
||||||
|
*/
|
||||||
|
fun scrollVolume(distance: Double)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* submit a scroll for brightness adjustment
|
||||||
|
*
|
||||||
|
* @param distance the scroll distance
|
||||||
|
*/
|
||||||
|
fun scrollBrightness(distance: Double)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reset all scroll distances to zero
|
||||||
|
*/
|
||||||
|
fun resetScroller()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handles scrolling of volume and brightness, adjusts them using the provided controllers and updates the overlay
|
||||||
|
*
|
||||||
|
* @param context context to create the scrollers in
|
||||||
|
* @param volumeController volume controller instance. if null, volume control is disabled
|
||||||
|
* @param screenController screen brightness controller instance. if null, brightness control is disabled
|
||||||
|
* @param overlayController overlay controller instance
|
||||||
|
* @param volumeDistance unit distance for volume scrolling, in dp
|
||||||
|
* @param brightnessDistance unit distance for brightness scrolling, in dp
|
||||||
|
*/
|
||||||
|
class VolumeAndBrightnessScrollerImpl(
|
||||||
|
context: Context,
|
||||||
|
private val volumeController: AudioVolumeController?,
|
||||||
|
private val screenController: ScreenBrightnessController?,
|
||||||
|
private val overlayController: SwipeControlsOverlay,
|
||||||
|
volumeDistance: Int = 10,
|
||||||
|
brightnessDistance: Int = 1
|
||||||
|
) : VolumeAndBrightnessScroller {
|
||||||
|
|
||||||
|
// region volume
|
||||||
|
private val volumeScroller =
|
||||||
|
ScrollDistanceHelper(
|
||||||
|
volumeDistance.applyDimension(
|
||||||
|
context,
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP
|
||||||
|
)
|
||||||
|
) { _, _, direction ->
|
||||||
|
volumeController?.run {
|
||||||
|
volume += direction
|
||||||
|
overlayController.onVolumeChanged(volume, maxVolume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun scrollVolume(distance: Double) = volumeScroller.add(distance)
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region brightness
|
||||||
|
private val brightnessScroller =
|
||||||
|
ScrollDistanceHelper(
|
||||||
|
brightnessDistance.applyDimension(
|
||||||
|
context,
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP
|
||||||
|
)
|
||||||
|
) { _, _, direction ->
|
||||||
|
screenController?.run {
|
||||||
|
if (screenBrightness > 0 || direction > 0) {
|
||||||
|
screenBrightness += direction
|
||||||
|
} else {
|
||||||
|
restoreDefaultBrightness()
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayController.onBrightnessChanged(screenBrightness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun scrollBrightness(distance: Double) = brightnessScroller.add(distance)
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
override fun resetScroller() {
|
||||||
|
volumeScroller.reset()
|
||||||
|
brightnessScroller.reset()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user