mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-12-01 00:02:55 +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.SwipeZonesController
|
||||
import app.revanced.integrations.swipecontrols.controller.VolumeKeysController
|
||||
import app.revanced.integrations.swipecontrols.controller.gesture.NoPtSSwipeGestureController
|
||||
import app.revanced.integrations.swipecontrols.controller.gesture.SwipeGestureController
|
||||
import app.revanced.integrations.swipecontrols.controller.gesture.ClassicSwipeController
|
||||
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.views.SwipeControlsOverlayLayout
|
||||
import app.revanced.integrations.utils.LogHelper
|
||||
@ -52,7 +53,7 @@ class SwipeControlsHostActivity : Activity() {
|
||||
/**
|
||||
* main gesture controller
|
||||
*/
|
||||
private lateinit var gesture: SwipeGestureController
|
||||
private lateinit var gesture: GestureController
|
||||
|
||||
/**
|
||||
* main volume keys controller
|
||||
@ -71,13 +72,12 @@ class SwipeControlsHostActivity : Activity() {
|
||||
// create controllers
|
||||
LogHelper.info(this.javaClass, "initializing swipe controls controllers")
|
||||
config = SwipeControlsConfigurationProvider(this)
|
||||
gesture = createGestureController()
|
||||
keys = VolumeKeysController(this)
|
||||
audio = createAudioController()
|
||||
screen = createScreenController()
|
||||
|
||||
// create overlay
|
||||
SwipeControlsOverlayLayout(this).let {
|
||||
SwipeControlsOverlayLayout(this, config).let {
|
||||
overlay = it
|
||||
contentRoot.addView(it)
|
||||
}
|
||||
@ -92,6 +92,9 @@ class SwipeControlsHostActivity : Activity() {
|
||||
)
|
||||
}
|
||||
|
||||
// create the gesture controller
|
||||
gesture = createGestureController()
|
||||
|
||||
// listen for changes in the player type
|
||||
PlayerType.onChange += this::onPlayerTypeChanged
|
||||
|
||||
@ -109,13 +112,13 @@ class SwipeControlsHostActivity : Activity() {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -163,8 +166,8 @@ class SwipeControlsHostActivity : Activity() {
|
||||
*/
|
||||
private fun createGestureController() =
|
||||
if (config.shouldEnablePressToSwipe)
|
||||
SwipeGestureController(this)
|
||||
else NoPtSSwipeGestureController(this)
|
||||
PressToSwipeController(this)
|
||||
else ClassicSwipeController(this)
|
||||
|
||||
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