diff --git a/app/src/main/java/app/revanced/integrations/shared/PlayerControlsVisibilityObserver.kt b/app/src/main/java/app/revanced/integrations/shared/PlayerControlsVisibilityObserver.kt new file mode 100644 index 00000000..b588665a --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/PlayerControlsVisibilityObserver.kt @@ -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(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(controlsLayoutParentId)?.let { parent -> + parent.findViewById(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 +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsHostActivity.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsHostActivity.kt index 3a3ec08d..01df7d94 100644 --- a/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsHostActivity.kt +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsHostActivity.kt @@ -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 { /** diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/ClassicSwipeController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/ClassicSwipeController.kt new file mode 100644 index 00000000..7e218578 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/ClassicSwipeController.kt @@ -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 + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/NoPtSSwipeGestureController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/NoPtSSwipeGestureController.kt deleted file mode 100644 index 9ba682ff..00000000 --- a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/NoPtSSwipeGestureController.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/PressToSwipeController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/PressToSwipeController.kt new file mode 100644 index 00000000..2575c814 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/PressToSwipeController.kt @@ -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 + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/SwipeGestureController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/SwipeGestureController.kt deleted file mode 100644 index b298079b..00000000 --- a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/SwipeGestureController.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/BaseGestureController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/BaseGestureController.kt new file mode 100644 index 00000000..46cd8dc3 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/BaseGestureController.kt @@ -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 +} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/GestureController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/GestureController.kt new file mode 100644 index 00000000..21cb33bd --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/GestureController.kt @@ -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 +} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/SwipeDetector.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/SwipeDetector.kt new file mode 100644 index 00000000..15318239 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/SwipeDetector.kt @@ -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 + } +} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt new file mode 100644 index 00000000..b5212dc4 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt @@ -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() + } +}