feat: disable swipe-controls when player controls are visible (#123)

This commit is contained in:
Chris 2022-09-21 01:56:42 +02:00 committed by GitHub
parent 2b763373ee
commit 6d8c0a0c73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 640 additions and 269 deletions

View File

@ -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
}

View File

@ -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 {
/**

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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()
}
}