From 268304376226457e99aea640aedfa3ccc22c59bb Mon Sep 17 00:00:00 2001 From: vkay94 Date: Wed, 21 Oct 2020 16:40:22 +0200 Subject: [PATCH 1/2] Player gestures: separate logic and UI --- .../player/event/BasePlayerGestureListener.kt | 434 ++++++++++++ .../newpipe/player/event/DisplayPortion.kt | 5 + .../player/event/PlayerGestureListener.java | 618 ++++-------------- 3 files changed, 569 insertions(+), 488 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt create mode 100644 app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt diff --git a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt new file mode 100644 index 000000000..8ce140443 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt @@ -0,0 +1,434 @@ +package org.schabi.newpipe.player.event + +import android.content.Context +import android.util.Log +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import org.schabi.newpipe.player.BasePlayer +import org.schabi.newpipe.player.MainPlayer +import org.schabi.newpipe.player.VideoPlayerImpl +import org.schabi.newpipe.player.helper.PlayerHelper +import org.schabi.newpipe.util.AnimationUtils +import kotlin.math.abs +import kotlin.math.hypot +import kotlin.math.max + +/** + * Base gesture handling for [VideoPlayerImpl] + * + * This class contains the logic for the player gestures like View preparations + * and provides some abstract methods to make it easier separating the logic from the UI. + */ +abstract class BasePlayerGestureListener( + @JvmField + protected val playerImpl: VideoPlayerImpl, + @JvmField + protected val service: MainPlayer +) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { + + // /////////////////////////////////////////////////////////////////// + // Abstract methods for VIDEO and POPUP + // /////////////////////////////////////////////////////////////////// + + abstract fun onDoubleTap(event: MotionEvent, portion: DisplayPortion) + + abstract fun onSingleTap(playerType: MainPlayer.PlayerType) + + abstract fun onScroll( + playerType: MainPlayer.PlayerType, + portion: DisplayPortion, + initialEvent: MotionEvent, + movingEvent: MotionEvent, + distanceX: Float, + distanceY: Float + ) + + abstract fun onScrollEnd(playerType: MainPlayer.PlayerType, event: MotionEvent) + + // /////////////////////////////////////////////////////////////////// + // Abstract methods for POPUP (exclusive) + // /////////////////////////////////////////////////////////////////// + + abstract fun onPopupResizingStart() + + abstract fun onPopupResizingEnd() + + private var initialPopupX: Int = -1 + private var initialPopupY: Int = -1 + + private var isMovingInMain = false + private var isMovingInPopup = false + private var isResizing = false + + private val tossFlingVelocity = PlayerHelper.getTossFlingVelocity(service) + + // [popup] initial coordinates and distance between fingers + private var initPointerDistance = -1.0 + private var initFirstPointerX = -1f + private var initFirstPointerY = -1f + private var initSecPointerX = -1f + private var initSecPointerY = -1f + + // /////////////////////////////////////////////////////////////////// + // onTouch implementation + // /////////////////////////////////////////////////////////////////// + + override fun onTouch(v: View, event: MotionEvent): Boolean { + return if (playerImpl.popupPlayerSelected()) { + onTouchInPopup(v, event) + } else { + onTouchInMain(v, event) + } + } + + private fun onTouchInMain(v: View, event: MotionEvent): Boolean { + playerImpl.gestureDetector.onTouchEvent(event) + if (event.action == MotionEvent.ACTION_UP && isMovingInMain) { + isMovingInMain = false + onScrollEnd(MainPlayer.PlayerType.VIDEO, event) + } + return when (event.action) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { + v.parent.requestDisallowInterceptTouchEvent(playerImpl.isFullscreen) + true + } + MotionEvent.ACTION_UP -> { + v.parent.requestDisallowInterceptTouchEvent(false) + false + } + else -> true + } + } + + private fun onTouchInPopup(v: View, event: MotionEvent): Boolean { + if (playerImpl == null) { + return false + } + playerImpl.gestureDetector.onTouchEvent(event) + if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) { + if (DEBUG) { + Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.") + } + onPopupResizingStart() + + // record coordinates of fingers + initFirstPointerX = event.getX(0) + initFirstPointerY = event.getY(0) + initSecPointerX = event.getX(1) + initSecPointerY = event.getY(1) + // record distance between fingers + initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX.toDouble(), + initFirstPointerY - initSecPointerY.toDouble()) + + isResizing = true + } + if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) { + if (DEBUG) { + Log.d(TAG, "onTouch() ACTION_MOVE > v = [$v], e1.getRaw = [${event.rawX}" + + ", ${event.rawY}]") + } + return handleMultiDrag(event) + } + if (event.action == MotionEvent.ACTION_UP) { + if (DEBUG) { + Log.d(TAG, "onTouch() ACTION_UP > v = [$v], e1.getRaw = [${event.rawX}" + + ", ${event.rawY}]") + } + if (isMovingInPopup) { + isMovingInPopup = false + onScrollEnd(MainPlayer.PlayerType.POPUP, event) + } + if (isResizing) { + isResizing = false + + initPointerDistance = (-1).toDouble() + initFirstPointerX = (-1).toFloat() + initFirstPointerY = (-1).toFloat() + initSecPointerX = (-1).toFloat() + initSecPointerY = (-1).toFloat() + + onPopupResizingEnd() + playerImpl.changeState(playerImpl.currentState) + } + if (!playerImpl.isPopupClosing) { + playerImpl.savePositionAndSize() + } + } + + v.performClick() + return true + } + + private fun handleMultiDrag(event: MotionEvent): Boolean { + if (initPointerDistance != -1.0 && event.pointerCount == 2) { + // get the movements of the fingers + val firstPointerMove = hypot(event.getX(0) - initFirstPointerX.toDouble(), + event.getY(0) - initFirstPointerY.toDouble()) + val secPointerMove = hypot(event.getX(1) - initSecPointerX.toDouble(), + event.getY(1) - initSecPointerY.toDouble()) + + // minimum threshold beyond which pinch gesture will work + val minimumMove = ViewConfiguration.get(service).scaledTouchSlop + + if (max(firstPointerMove, secPointerMove) > minimumMove) { + // calculate current distance between the pointers + val currentPointerDistance = hypot(event.getX(0) - event.getX(1).toDouble(), + event.getY(0) - event.getY(1).toDouble()) + + val popupWidth = playerImpl.popupWidth.toDouble() + // change co-ordinates of popup so the center stays at the same position + val newWidth = popupWidth * currentPointerDistance / initPointerDistance + initPointerDistance = currentPointerDistance + playerImpl.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt() + + playerImpl.checkPopupPositionBounds() + playerImpl.updateScreenSize() + + playerImpl.updatePopupSize( + Math.min(playerImpl.screenWidth.toDouble(), newWidth).toInt(), + -1) + return true + } + } + return false + } + + // /////////////////////////////////////////////////////////////////// + // Simple gestures + // /////////////////////////////////////////////////////////////////// + + override fun onDown(e: MotionEvent): Boolean { + if (DEBUG) + Log.d(TAG, "onDown called with e = [$e]") + + return if (playerImpl.popupPlayerSelected()) + onDownInPopup(e) + else + true + } + + private fun onDownInPopup(e: MotionEvent): Boolean { + // Fix popup position when the user touch it, it may have the wrong one + // because the soft input is visible (the draggable area is currently resized). + playerImpl.updateScreenSize() + playerImpl.checkPopupPositionBounds() + initialPopupX = playerImpl.popupLayoutParams.x + initialPopupY = playerImpl.popupLayoutParams.y + playerImpl.popupWidth = playerImpl.popupLayoutParams.width.toFloat() + playerImpl.popupHeight = playerImpl.popupLayoutParams.height.toFloat() + return super.onDown(e) + } + + override fun onDoubleTap(e: MotionEvent): Boolean { + if (DEBUG) + Log.d(TAG, "onDoubleTap called with e = [$e]") + + onDoubleTap(e, getDisplayPortion(e)) + return true + } + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + if (DEBUG) + Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]") + + if (playerImpl.popupPlayerSelected()) { + if (playerImpl.player == null) + return false + + onSingleTap(MainPlayer.PlayerType.POPUP) + return true + } else { + super.onSingleTapConfirmed(e) + if (playerImpl.currentState == BasePlayer.STATE_BLOCKED) + return true + + onSingleTap(MainPlayer.PlayerType.VIDEO) + } + return true + } + + override fun onLongPress(e: MotionEvent?) { + if (playerImpl.popupPlayerSelected()) { + playerImpl.updateScreenSize() + playerImpl.checkPopupPositionBounds() + playerImpl.updatePopupSize(playerImpl.screenWidth.toInt(), -1) + } + } + + override fun onScroll( + initialEvent: MotionEvent, + movingEvent: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + return if (playerImpl.popupPlayerSelected()) { + onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY) + } else { + onScrollInMain(initialEvent, movingEvent, distanceX, distanceY) + } + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent?, + velocityX: Float, + velocityY: Float + ): Boolean { + return if (playerImpl.popupPlayerSelected()) { + val absVelocityX = abs(velocityX) + val absVelocityY = abs(velocityY) + if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) { + if (absVelocityX > tossFlingVelocity) { + playerImpl.popupLayoutParams.x = velocityX.toInt() + } + if (absVelocityY > tossFlingVelocity) { + playerImpl.popupLayoutParams.y = velocityY.toInt() + } + playerImpl.checkPopupPositionBounds() + playerImpl.windowManager + .updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams) + return true + } + return false + } else { + true + } + } + + private fun onScrollInMain( + initialEvent: MotionEvent, + movingEvent: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + + if (!playerImpl.isFullscreen) { + return false + } + + val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service) + val isTouchingNavigationBar: Boolean = (initialEvent.y + > playerImpl.rootView.height - getNavigationBarHeight(service)) + if (isTouchingStatusBar || isTouchingNavigationBar) { + return false + } + + val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD + if (!isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) || + playerImpl.currentState == BasePlayer.STATE_COMPLETED) { + return false + } + + isMovingInMain = true + + onScroll(MainPlayer.PlayerType.VIDEO, getDisplayHalfPortion(initialEvent), + initialEvent, movingEvent, distanceX, distanceY) + + return true + } + + private fun onScrollInPopup( + initialEvent: MotionEvent, + movingEvent: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + + if (isResizing) { + return super.onScroll(initialEvent, movingEvent, distanceX, distanceY) + } + + if (!isMovingInPopup) { + AnimationUtils.animateView(playerImpl.closeOverlayButton, true, 200) + } + + isMovingInPopup = true + + val diffX: Float = (movingEvent.rawX - initialEvent.rawX) + var posX: Float = (initialPopupX + diffX) + val diffY: Float = (movingEvent.rawY - initialEvent.rawY) + var posY: Float = (initialPopupY + diffY) + + if (posX > playerImpl.screenWidth - playerImpl.popupWidth) { + posX = (playerImpl.screenWidth - playerImpl.popupWidth) + } else if (posX < 0) { + posX = 0f + } + + if (posY > playerImpl.screenHeight - playerImpl.popupHeight) { + posY = (playerImpl.screenHeight - playerImpl.popupHeight) + } else if (posY < 0) { + posY = 0f + } + + playerImpl.popupLayoutParams.x = posX.toInt() + playerImpl.popupLayoutParams.y = posY.toInt() + + onScroll(MainPlayer.PlayerType.POPUP, getDisplayHalfPortion(initialEvent), + initialEvent, movingEvent, distanceX, distanceY) + + playerImpl.windowManager + .updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams) + return true + } + + // /////////////////////////////////////////////////////////////////// + // Utils + // /////////////////////////////////////////////////////////////////// + + private fun getDisplayPortion(e: MotionEvent): DisplayPortion { + return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) { + when { + e.x < playerImpl.popupWidth / 3.0 -> DisplayPortion.LEFT + e.x > playerImpl.popupWidth * 2.0 / 3.0 -> DisplayPortion.RIGHT + else -> DisplayPortion.MIDDLE + } + } else /* MainPlayer.PlayerType.VIDEO */ { + when { + e.x < playerImpl.rootView.width / 3.0 -> DisplayPortion.LEFT + e.x > playerImpl.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT + else -> DisplayPortion.MIDDLE + } + } + } + + // Currently needed for scrolling since there is no action more the middle portion + private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion { + return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) { + when { + e.x < playerImpl.popupWidth / 2.0 -> DisplayPortion.LEFT_HALF + else -> DisplayPortion.RIGHT_HALF + } + } else /* MainPlayer.PlayerType.VIDEO */ { + when { + e.x < playerImpl.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF + else -> DisplayPortion.RIGHT_HALF + } + } + } + + private fun getNavigationBarHeight(context: Context): Int { + val resId = context.resources + .getIdentifier("navigation_bar_height", "dimen", "android") + return if (resId > 0) { + context.resources.getDimensionPixelSize(resId) + } else 0 + } + + private fun getStatusBarHeight(context: Context): Int { + val resId = context.resources + .getIdentifier("status_bar_height", "dimen", "android") + return if (resId > 0) { + context.resources.getDimensionPixelSize(resId) + } else 0 + } + + companion object { + private const val TAG = "BasePlayerGestListener" + private val DEBUG = BasePlayer.DEBUG + + private const val MOVEMENT_THRESHOLD = 40 + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt b/app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt new file mode 100644 index 000000000..f15e42897 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt @@ -0,0 +1,5 @@ +package org.schabi.newpipe.player.event + +enum class DisplayPortion { + LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java index a2def2a64..003b9ff9d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -1,16 +1,16 @@ package org.schabi.newpipe.player.event; import android.app.Activity; -import android.content.Context; import android.util.Log; -import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; -import android.view.ViewConfiguration; import android.view.Window; import android.view.WindowManager; import android.widget.ProgressBar; + import androidx.appcompat.content.res.AppCompatResources; + +import org.jetbrains.annotations.NotNull; import org.schabi.newpipe.R; import org.schabi.newpipe.player.BasePlayer; import org.schabi.newpipe.player.MainPlayer; @@ -23,217 +23,114 @@ import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.animateView; +/** + * GestureListener for the player + * + * While {@link BasePlayerGestureListener} contains the logic behind the single gestures + * this class focuses on the visual aspect like hiding and showing the controls or changing + * volume/brightness during scrolling for specific events. + */ public class PlayerGestureListener - extends GestureDetector.SimpleOnGestureListener + extends BasePlayerGestureListener implements View.OnTouchListener { private static final String TAG = ".PlayerGestureListener"; private static final boolean DEBUG = BasePlayer.DEBUG; - private final VideoPlayerImpl playerImpl; - private final MainPlayer service; - - private int initialPopupX; - private int initialPopupY; - - private boolean isMovingInMain; - private boolean isMovingInPopup; - - private boolean isResizing; - - private final int tossFlingVelocity; - private final boolean isVolumeGestureEnabled; private final boolean isBrightnessGestureEnabled; private final int maxVolume; - private static final int MOVEMENT_THRESHOLD = 40; - - // [popup] initial coordinates and distance between fingers - private double initPointerDistance = -1; - private float initFirstPointerX = -1; - private float initFirstPointerY = -1; - private float initSecPointerX = -1; - private float initSecPointerY = -1; - public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) { - this.playerImpl = playerImpl; - this.service = service; - this.tossFlingVelocity = PlayerHelper.getTossFlingVelocity(service); + super(playerImpl, service); isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service); isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(service); maxVolume = playerImpl.getAudioReactor().getMaxVolume(); } - /*////////////////////////////////////////////////////////////////////////// - // Helpers - //////////////////////////////////////////////////////////////////////////*/ - - /* - * Main and popup players' gesture listeners is too different. - * So it will be better to have different implementations of them - * */ @Override - public boolean onDoubleTap(final MotionEvent e) { + public void onDoubleTap(@NotNull final MotionEvent event, + @NotNull final DisplayPortion portion) { if (DEBUG) { - Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " - + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); + Log.d(TAG, "onDoubleTap called with playerType = [" + + playerImpl.getPlayerType() + "], portion = [" + + portion + "]"); + } + if (playerImpl.isSomePopupMenuVisible()) { + playerImpl.hideControls(0, 0); } - if (playerImpl.popupPlayerSelected()) { - return onDoubleTapInPopup(e); - } else { - return onDoubleTapInMain(e); - } - } - - @Override - public boolean onSingleTapConfirmed(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); - } - - if (playerImpl.popupPlayerSelected()) { - return onSingleTapConfirmedInPopup(e); - } else { - return onSingleTapConfirmedInMain(e); - } - } - - @Override - public boolean onDown(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onDown() called with: e = [" + e + "]"); - } - - if (playerImpl.popupPlayerSelected()) { - return onDownInPopup(e); - } else { - return true; - } - } - - @Override - public void onLongPress(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); - } - - if (playerImpl.popupPlayerSelected()) { - onLongPressInPopup(e); - } - } - - @Override - public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent, - final float distanceX, final float distanceY) { - if (playerImpl.popupPlayerSelected()) { - return onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY); - } else { - return onScrollInMain(initialEvent, movingEvent, distanceX, distanceY); - } - } - - @Override - public boolean onFling(final MotionEvent e1, final MotionEvent e2, - final float velocityX, final float velocityY) { - if (DEBUG) { - Log.d(TAG, "onFling() called with velocity: dX=[" - + velocityX + "], dY=[" + velocityY + "]"); - } - - if (playerImpl.popupPlayerSelected()) { - return onFlingInPopup(e1, e2, velocityX, velocityY); - } else { - return true; - } - } - - @Override - public boolean onTouch(final View v, final MotionEvent event) { - /*if (DEBUG && false) { - Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]"); - }*/ - - if (playerImpl.popupPlayerSelected()) { - return onTouchInPopup(v, event); - } else { - return onTouchInMain(v, event); - } - } - - - /*////////////////////////////////////////////////////////////////////////// - // Main player listener - //////////////////////////////////////////////////////////////////////////*/ - - private boolean onDoubleTapInMain(final MotionEvent e) { - if (e.getX() > playerImpl.getRootView().getWidth() * 2.0 / 3.0) { - playerImpl.onFastForward(); - } else if (e.getX() < playerImpl.getRootView().getWidth() / 3.0) { + if (portion == DisplayPortion.LEFT) { playerImpl.onFastRewind(); - } else { - playerImpl.getPlayPauseButton().performClick(); + } else if (portion == DisplayPortion.RIGHT) { + playerImpl.onFastForward(); } - - return true; } - - private boolean onSingleTapConfirmedInMain(final MotionEvent e) { + @Override + public void onSingleTap(@NotNull final MainPlayer.PlayerType playerType) { if (DEBUG) { - Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + Log.d(TAG, "onSingleTap called with playerType = [" + + playerImpl.getPlayerType() + "]"); } + if (playerType == MainPlayer.PlayerType.POPUP) { - if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { - return true; - } - - if (playerImpl.isControlsVisible()) { - playerImpl.hideControls(150, 0); - } else { - if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { - playerImpl.showControls(0); + if (playerImpl.isControlsVisible()) { + playerImpl.hideControls(100, 100); } else { + playerImpl.getPlayPauseButton().requestFocus(); playerImpl.showControlsThenHide(); } + + } else /* playerType == MainPlayer.PlayerType.VIDEO */ { + + if (playerImpl.isControlsVisible()) { + playerImpl.hideControls(150, 0); + } else { + if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { + playerImpl.showControls(0); + } else { + playerImpl.showControlsThenHide(); + } + } } - return true; } - private boolean onScrollInMain(final MotionEvent initialEvent, final MotionEvent movingEvent, - final float distanceX, final float distanceY) { - if ((!isVolumeGestureEnabled && !isBrightnessGestureEnabled) - || !playerImpl.isFullscreen()) { - return false; + @Override + public void onScroll(@NotNull final MainPlayer.PlayerType playerType, + @NotNull final DisplayPortion portion, + @NotNull final MotionEvent initialEvent, + @NotNull final MotionEvent movingEvent, + final float distanceX, final float distanceY) { + if (DEBUG) { + Log.d(TAG, "onScroll called with playerType = [" + + playerImpl.getPlayerType() + "], portion = [" + + portion + "]"); } + if (playerType == MainPlayer.PlayerType.VIDEO) { + if (portion == DisplayPortion.LEFT_HALF) { + onScrollMainVolume(distanceX, distanceY); - final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(service); - final boolean isTouchingNavigationBar = initialEvent.getY() - > playerImpl.getRootView().getHeight() - getNavigationBarHeight(service); - if (isTouchingStatusBar || isTouchingNavigationBar) { - return false; + } else /* DisplayPortion.RIGHT_HALF */ { + onScrollMainBrightness(distanceX, distanceY); + } + + } else /* MainPlayer.PlayerType.POPUP */ { + final View closingOverlayView = playerImpl.getClosingOverlayView(); + if (playerImpl.isInsideClosingRadius(movingEvent)) { + if (closingOverlayView.getVisibility() == View.GONE) { + animateView(closingOverlayView, true, 250); + } + } else { + if (closingOverlayView.getVisibility() == View.VISIBLE) { + animateView(closingOverlayView, false, 0); + } + } } + } - /*if (DEBUG && false) Log.d(TAG, "onScrollInMain = " + - ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + - ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + - ", distanceXy = [" + distanceX + ", " + distanceY + "]");*/ - - final boolean insideThreshold = - Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; - if (!isMovingInMain && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) - || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { - return false; - } - - isMovingInMain = true; - - final boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled; - final boolean acceptVolumeArea = acceptAnyArea - || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2.0; - - if (isVolumeGestureEnabled && acceptVolumeArea) { + private void onScrollMainVolume(final float distanceX, final float distanceY) { + if (isVolumeGestureEnabled) { playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); final float currentProgressPercent = (float) playerImpl .getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); @@ -258,10 +155,14 @@ public class PlayerGestureListener if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); } - } else { + } + } + + private void onScrollMainBrightness(final float distanceX, final float distanceY) { + if (isBrightnessGestureEnabled) { final Activity parent = playerImpl.getParentActivity(); if (parent == null) { - return true; + return; } final Window window = parent.getWindow(); @@ -299,330 +200,71 @@ public class PlayerGestureListener playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); } } - return true; } - private void onScrollEndInMain() { + @Override + public void onScrollEnd(@NotNull final MainPlayer.PlayerType playerType, + @NotNull final MotionEvent event) { if (DEBUG) { - Log.d(TAG, "onScrollEnd() called"); + Log.d(TAG, "onScrollEnd called with playerType = [" + + playerImpl.getPlayerType() + "]"); } + if (playerType == MainPlayer.PlayerType.VIDEO) { + if (DEBUG) { + Log.d(TAG, "onScrollEnd() called"); + } - if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); - } - if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); - } + if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, + false, 200, 200); + } + if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, + false, 200, 200); + } - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + } else { + if (playerImpl == null) { + return; + } + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + + if (playerImpl.isInsideClosingRadius(event)) { + playerImpl.closePopup(); + } else { + animateView(playerImpl.getClosingOverlayView(), false, 0); + + if (!playerImpl.isPopupClosing) { + animateView(playerImpl.getCloseOverlayButton(), false, 200); + } + } } } - private boolean onTouchInMain(final View v, final MotionEvent event) { - playerImpl.getGestureDetector().onTouchEvent(event); - if (event.getAction() == MotionEvent.ACTION_UP && isMovingInMain) { - isMovingInMain = false; - onScrollEndInMain(); - } - // This hack allows to stop receiving touch events on appbar - // while touching video player's view - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_MOVE: - v.getParent().requestDisallowInterceptTouchEvent(playerImpl.isFullscreen()); - return true; - case MotionEvent.ACTION_UP: - v.getParent().requestDisallowInterceptTouchEvent(false); - return false; - default: - return true; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Popup player listener - //////////////////////////////////////////////////////////////////////////*/ - - private boolean onDoubleTapInPopup(final MotionEvent e) { - if (playerImpl == null || !playerImpl.isPlaying()) { - return false; + @Override + public void onPopupResizingStart() { + if (DEBUG) { + Log.d(TAG, "onPopupResizingStart called"); } + playerImpl.showAndAnimateControl(-1, true); + playerImpl.getLoadingPanel().setVisibility(View.GONE); playerImpl.hideControls(0, 0); - - if (e.getX() > playerImpl.getPopupWidth() / 2) { - playerImpl.onFastForward(); - } else { - playerImpl.onFastRewind(); - } - - return true; + animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); + animateView(playerImpl.getResizingIndicator(), true, 200, 0); } - private boolean onSingleTapConfirmedInPopup(final MotionEvent e) { - if (playerImpl == null || playerImpl.getPlayer() == null) { - return false; + @Override + public void onPopupResizingEnd() { + if (DEBUG) { + Log.d(TAG, "onPopupResizingEnd called"); } - if (playerImpl.isControlsVisible()) { - playerImpl.hideControls(100, 100); - } else { - playerImpl.getPlayPauseButton().requestFocus(); - playerImpl.showControlsThenHide(); - } - return true; - } - - private boolean onDownInPopup(final MotionEvent e) { - // Fix popup position when the user touch it, it may have the wrong one - // because the soft input is visible (the draggable area is currently resized). - playerImpl.updateScreenSize(); - playerImpl.checkPopupPositionBounds(); - - initialPopupX = playerImpl.getPopupLayoutParams().x; - initialPopupY = playerImpl.getPopupLayoutParams().y; - playerImpl.setPopupWidth(playerImpl.getPopupLayoutParams().width); - playerImpl.setPopupHeight(playerImpl.getPopupLayoutParams().height); - return super.onDown(e); - } - - private void onLongPressInPopup(final MotionEvent e) { - playerImpl.updateScreenSize(); - playerImpl.checkPopupPositionBounds(); - playerImpl.updatePopupSize((int) playerImpl.getScreenWidth(), -1); - } - - private boolean onScrollInPopup(final MotionEvent initialEvent, - final MotionEvent movingEvent, - final float distanceX, - final float distanceY) { - if (isResizing || playerImpl == null) { - return super.onScroll(initialEvent, movingEvent, distanceX, distanceY); - } - - if (!isMovingInPopup) { - animateView(playerImpl.getCloseOverlayButton(), true, 200); - } - - isMovingInPopup = true; - - final float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()); - float posX = (int) (initialPopupX + diffX); - final float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()); - float posY = (int) (initialPopupY + diffY); - - if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) { - posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth()); - } else if (posX < 0) { - posX = 0; - } - - if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) { - posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight()); - } else if (posY < 0) { - posY = 0; - } - - playerImpl.getPopupLayoutParams().x = (int) posX; - playerImpl.getPopupLayoutParams().y = (int) posY; - - final View closingOverlayView = playerImpl.getClosingOverlayView(); - if (playerImpl.isInsideClosingRadius(movingEvent)) { - if (closingOverlayView.getVisibility() == View.GONE) { - animateView(closingOverlayView, true, 250); - } - } else { - if (closingOverlayView.getVisibility() == View.VISIBLE) { - animateView(closingOverlayView, false, 0); - } - } - -// if (DEBUG) { -// Log.d(TAG, "onScrollInPopup = " -// + "e1.getRaw = [" + initialEvent.getRawX() + ", " -// + initialEvent.getRawY() + "], " -// + "e1.getX,Y = [" + initialEvent.getX() + ", " -// + initialEvent.getY() + "], " -// + "e2.getRaw = [" + movingEvent.getRawX() + ", " -// + movingEvent.getRawY() + "], " -// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], " -// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], " -// + "posX,Y = [" + posX + ", " + posY + "], " -// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]"); -// } - playerImpl.windowManager - .updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams()); - return true; - } - - private void onScrollEndInPopup(final MotionEvent event) { - if (playerImpl == null) { - return; - } - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - - if (playerImpl.isInsideClosingRadius(event)) { - playerImpl.closePopup(); - } else { - animateView(playerImpl.getClosingOverlayView(), false, 0); - - if (!playerImpl.isPopupClosing) { - animateView(playerImpl.getCloseOverlayButton(), false, 200); - } - } - } - - private boolean onFlingInPopup(final MotionEvent e1, - final MotionEvent e2, - final float velocityX, - final float velocityY) { - if (playerImpl == null) { - return false; - } - - final float absVelocityX = Math.abs(velocityX); - final float absVelocityY = Math.abs(velocityY); - if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { - if (absVelocityX > tossFlingVelocity) { - playerImpl.getPopupLayoutParams().x = (int) velocityX; - } - if (absVelocityY > tossFlingVelocity) { - playerImpl.getPopupLayoutParams().y = (int) velocityY; - } - playerImpl.checkPopupPositionBounds(); - playerImpl.windowManager - .updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams()); - return true; - } - return false; - } - - private boolean onTouchInPopup(final View v, final MotionEvent event) { - if (playerImpl == null) { - return false; - } - playerImpl.getGestureDetector().onTouchEvent(event); - - if (event.getPointerCount() == 2 && !isMovingInPopup && !isResizing) { - if (DEBUG) { - Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); - } - playerImpl.showAndAnimateControl(-1, true); - playerImpl.getLoadingPanel().setVisibility(View.GONE); - - playerImpl.hideControls(0, 0); - animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); - animateView(playerImpl.getResizingIndicator(), true, 200, 0); - //record coordinates of fingers - initFirstPointerX = event.getX(0); - initFirstPointerY = event.getY(0); - initSecPointerX = event.getX(1); - initSecPointerY = event.getY(1); - //record distance between fingers - initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX, - initFirstPointerY - initSecPointerY); - - isResizing = true; - } - - if (event.getAction() == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) { - if (DEBUG) { - Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], " - + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); - } - return handleMultiDrag(event); - } - - if (event.getAction() == MotionEvent.ACTION_UP) { - if (DEBUG) { - Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], " - + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); - } - if (isMovingInPopup) { - isMovingInPopup = false; - onScrollEndInPopup(event); - } - - if (isResizing) { - isResizing = false; - - initPointerDistance = -1; - initFirstPointerX = -1; - initFirstPointerY = -1; - initSecPointerX = -1; - initSecPointerY = -1; - - animateView(playerImpl.getResizingIndicator(), false, 100, 0); - playerImpl.changeState(playerImpl.getCurrentState()); - } - - if (!playerImpl.isPopupClosing) { - playerImpl.savePositionAndSize(); - } - } - - v.performClick(); - return true; - } - - private boolean handleMultiDrag(final MotionEvent event) { - if (initPointerDistance != -1 && event.getPointerCount() == 2) { - // get the movements of the fingers - final double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX, - event.getY(0) - initFirstPointerY); - final double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX, - event.getY(1) - initSecPointerY); - - // minimum threshold beyond which pinch gesture will work - final int minimumMove = ViewConfiguration.get(service).getScaledTouchSlop(); - - if (Math.max(firstPointerMove, secPointerMove) > minimumMove) { - // calculate current distance between the pointers - final double currentPointerDistance = - Math.hypot(event.getX(0) - event.getX(1), - event.getY(0) - event.getY(1)); - - final double popupWidth = playerImpl.getPopupWidth(); - // change co-ordinates of popup so the center stays at the same position - final double newWidth = (popupWidth * currentPointerDistance / initPointerDistance); - initPointerDistance = currentPointerDistance; - playerImpl.getPopupLayoutParams().x += (popupWidth - newWidth) / 2; - - playerImpl.checkPopupPositionBounds(); - playerImpl.updateScreenSize(); - - playerImpl.updatePopupSize( - (int) Math.min(playerImpl.getScreenWidth(), newWidth), - -1); - return true; - } - } - return false; - } - - - /* - * Utils - * */ - - private int getNavigationBarHeight(final Context context) { - final int resId = context.getResources() - .getIdentifier("navigation_bar_height", "dimen", "android"); - if (resId > 0) { - return context.getResources().getDimensionPixelSize(resId); - } - return 0; - } - - private int getStatusBarHeight(final Context context) { - final int resId = context.getResources() - .getIdentifier("status_bar_height", "dimen", "android"); - if (resId > 0) { - return context.getResources().getDimensionPixelSize(resId); - } - return 0; + animateView(playerImpl.getResizingIndicator(), false, 100, 0); } } From 347566c311f9e215b48baa20ab3c10f53525f809 Mon Sep 17 00:00:00 2001 From: vkay94 Date: Wed, 21 Oct 2020 16:41:14 +0200 Subject: [PATCH 2/2] Player gestures: Add multi-double-tap logic --- .../player/event/BasePlayerGestureListener.kt | 69 +++++++++++++++++++ .../newpipe/player/event/DoubleTapListener.kt | 7 ++ .../player/event/PlayerGestureListener.java | 2 + 3 files changed, 78 insertions(+) create mode 100644 app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt diff --git a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt index 8ce140443..dcc1bb128 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt +++ b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.event import android.content.Context +import android.os.Handler import android.util.Log import android.view.GestureDetector import android.view.MotionEvent @@ -203,6 +204,11 @@ abstract class BasePlayerGestureListener( if (DEBUG) Log.d(TAG, "onDown called with e = [$e]") + if (isDoubleTapping && isDoubleTapEnabled) { + doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e)) + return true + } + return if (playerImpl.popupPlayerSelected()) onDownInPopup(e) else @@ -233,6 +239,9 @@ abstract class BasePlayerGestureListener( if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]") + if (isDoubleTapping) + return true + if (playerImpl.popupPlayerSelected()) { if (playerImpl.player == null) return false @@ -374,6 +383,65 @@ abstract class BasePlayerGestureListener( return true } + // /////////////////////////////////////////////////////////////////// + // Multi double tapping + // /////////////////////////////////////////////////////////////////// + + var doubleTapControls: DoubleTapListener? = null + private set + + val isDoubleTapEnabled: Boolean + get() = doubleTapDelay > 0 + + var isDoubleTapping = false + private set + + fun doubleTapControls(listener: DoubleTapListener) = apply { + doubleTapControls = listener + } + + private var doubleTapDelay = DOUBLE_TAP_DELAY + private val doubleTapHandler: Handler = Handler() + private val doubleTapRunnable = Runnable { + if (DEBUG) + Log.d(TAG, "doubleTapRunnable called") + + isDoubleTapping = false + doubleTapControls?.onDoubleTapFinished() + } + + fun startMultiDoubleTap(e: MotionEvent) { + if (!isDoubleTapping) { + if (DEBUG) + Log.d(TAG, "startMultiDoubleTap called with e = [$e]") + + keepInDoubleTapMode() + doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e)) + } + } + + fun keepInDoubleTapMode() { + if (DEBUG) + Log.d(TAG, "keepInDoubleTapMode called") + + isDoubleTapping = true + doubleTapHandler.removeCallbacks(doubleTapRunnable) + doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay) + } + + fun endMultiDoubleTap() { + if (DEBUG) + Log.d(TAG, "endMultiDoubleTap called") + + isDoubleTapping = false + doubleTapHandler.removeCallbacks(doubleTapRunnable) + doubleTapControls?.onDoubleTapFinished() + } + + fun enableMultiDoubleTap(enable: Boolean) = apply { + doubleTapDelay = if (enable) DOUBLE_TAP_DELAY else 0 + } + // /////////////////////////////////////////////////////////////////// // Utils // /////////////////////////////////////////////////////////////////// @@ -429,6 +497,7 @@ abstract class BasePlayerGestureListener( private const val TAG = "BasePlayerGestListener" private val DEBUG = BasePlayer.DEBUG + private const val DOUBLE_TAP_DELAY = 550L private const val MOVEMENT_THRESHOLD = 40 } } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt new file mode 100644 index 000000000..84cfb9b8d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt @@ -0,0 +1,7 @@ +package org.schabi.newpipe.player.event + +interface DoubleTapListener { + fun onDoubleTapStarted(portion: DisplayPortion) {} + fun onDoubleTapProgressDown(portion: DisplayPortion) {} + fun onDoubleTapFinished() {} +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java index 003b9ff9d..a23d5d3ff 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -62,6 +62,8 @@ public class PlayerGestureListener if (portion == DisplayPortion.LEFT) { playerImpl.onFastRewind(); + } else if (portion == DisplayPortion.MIDDLE) { + playerImpl.onPlayPause(); } else if (portion == DisplayPortion.RIGHT) { playerImpl.onFastForward(); }