From 2c7f02a24d8a7c412b59c24840dfe0e900c4d285 Mon Sep 17 00:00:00 2001 From: Koen <koen@pop-os.localdomain> Date: Tue, 16 Jan 2024 11:01:00 +0100 Subject: [PATCH] Added zoom pan snapping. --- .../views/behavior/GestureControlView.kt | 106 +++++++++++++++--- .../views/video/FutoVideoPlayer.kt | 7 ++ .../views/video/FutoVideoPlayerBase.kt | 9 ++ .../drawable/background_primary_border.xml | 5 + .../main/res/layout/view_gesture_controls.xml | 18 +++ 5 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 app/src/main/res/drawable/background_primary_border.xml diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt index 10953833..5668a89a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt @@ -9,6 +9,7 @@ import android.graphics.Matrix import android.graphics.drawable.Animatable import android.media.AudioManager import android.util.AttributeSet +import android.util.Log import android.view.GestureDetector import android.view.LayoutInflater import android.view.MotionEvent @@ -85,6 +86,10 @@ class GestureControlView : LinearLayout { private val _layoutControlsZoom: FrameLayout private val _textZoom: TextView private var _isZooming = false + private var _isZoomPanEnabled = false + private var _surfaceView: View? = null + private var _layoutIndicatorFill: FrameLayout; + private var _layoutIndicatorFit: FrameLayout; private val _gestureController: GestureDetectorCompat; @@ -113,20 +118,16 @@ class GestureControlView : LinearLayout { _textZoom = findViewById(R.id.text_zoom) _progressBrightness = findViewById(R.id.progress_brightness); _layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen); + _layoutIndicatorFill = findViewById(R.id.layout_indicator_fill); + _layoutIndicatorFit = findViewById(R.id.layout_indicator_fit); _scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { - if (!_isFullScreen || !Settings.instance.gestureControls.zoom) { + if (!_isZoomPanEnabled || !_isFullScreen || !Settings.instance.gestureControls.zoom) { return false } - var newScaleFactor = (_scaleFactor * detector.scaleFactor).coerceAtLeast(1.0f).coerceAtMost(5.0f) - - //Make original zoom sticky - if (newScaleFactor - 1.0f < 0.01f) { - newScaleFactor = 1.0f - } - + val newScaleFactor = (_scaleFactor * detector.scaleFactor).coerceAtLeast(1.0f).coerceAtMost(10.0f) val scaleFactorChange = newScaleFactor / _scaleFactor _scaleFactor = newScaleFactor onZoom.emit(_scaleFactor) @@ -149,6 +150,25 @@ class GestureControlView : LinearLayout { _layoutControlsZoom.visibility = View.VISIBLE _textZoom.text = "${String.format("%.1f", _scaleFactor)}x" _isZooming = true + + if (willSnapFill()) { + _layoutIndicatorFill.visibility = View.VISIBLE + _layoutIndicatorFit.visibility = View.GONE + } else if (willSnapFit()) { + _layoutIndicatorFill.visibility = View.GONE + _layoutIndicatorFit.visibility = View.VISIBLE + + _surfaceView?.let { + val lp = _layoutIndicatorFit.layoutParams + lp.width = it.width + lp.height = it.height + _layoutIndicatorFit.layoutParams = lp + } + } else { + _layoutIndicatorFill.visibility = View.GONE + _layoutIndicatorFit.visibility = View.GONE + } + return true } }) @@ -201,7 +221,7 @@ class GestureControlView : LinearLayout { } } } - } else if (_isFullScreen && !_isZooming && Settings.instance.gestureControls.pan) { + } else if (_isZoomPanEnabled && _isFullScreen && !_isZooming && Settings.instance.gestureControls.pan) { stopAllGestures() pan(_translationX - distanceX, _translationY - distanceY) } @@ -244,6 +264,19 @@ class GestureControlView : LinearLayout { isClickable = true } + fun setZoomPanEnabled(view: View) { + _isZoomPanEnabled = true + _surfaceView = view + } + + fun resetZoomPan() { + _scaleFactor = 1.0f + onZoom.emit(_scaleFactor) + _translationX = 0f + _translationY = 0f + onPan.emit(_translationX, _translationY) + } + private fun pan(translationX: Float, translationY: Float) { val xc = width / 2.0f val yc = height / 2.0f @@ -258,6 +291,8 @@ class GestureControlView : LinearLayout { _translationY = translationY.coerceAtLeast(ymin).coerceAtMost(ymax) onPan.emit(_translationX, _translationY) + + Log.i(TAG, "surfaceView (width: ${_surfaceView?.width}, height: ${_surfaceView?.height}, x: ${_surfaceView?.x}, y: ${_surfaceView?.y}") } fun setupTouchArea(layoutControls: ViewGroup? = null, background: View? = null) { @@ -306,7 +341,26 @@ class GestureControlView : LinearLayout { } if (_isZooming && ev.action == MotionEvent.ACTION_UP) { + val surfaceView = _surfaceView + if (surfaceView != null && willSnapFill()) { + _scaleFactor = calculateZoomScaleFactor() + onZoom.emit(_scaleFactor) + + _translationX = 0f + _translationY = 0f + onPan.emit(_translationX, _translationY) + } else if (willSnapFit()) { + _scaleFactor = 1f + onZoom.emit(_scaleFactor) + + _translationX = 0f + _translationY = 0f + onPan.emit(_translationX, _translationY) + } + _layoutControlsZoom.visibility = View.GONE + _layoutIndicatorFill.visibility = View.GONE + _layoutIndicatorFit.visibility = View.GONE _isZooming = false } @@ -317,6 +371,34 @@ class GestureControlView : LinearLayout { return true; } + private fun calculateZoomScaleFactor(): Float { + val w = _surfaceView?.width?.toFloat() ?: return 1.0f; + val h = _surfaceView?.height?.toFloat() ?: return 1.0f; + if (w == 0.0f || h == 0.0f) { + return 1.0f; + } + + return Math.max(width / w, height / h) + } + + private fun willSnapFill(): Boolean { + //TODO: Make sure pan is not too far away from 0, 0 + val surfaceView = _surfaceView + if (surfaceView != null) { + val zoomScaleFactor = calculateZoomScaleFactor() + if (Math.abs(_scaleFactor - zoomScaleFactor) < 0.05f) { + return true + } + } + + return false + } + + private fun willSnapFit(): Boolean { + //TODO: Make sure pan is not too far away from 0, 0 + return Math.abs(_scaleFactor - 1.0f) < 0.05f + } + fun cancelHideJob() { _jobHideControls?.cancel(); _jobHideControls = null; @@ -646,11 +728,7 @@ class GestureControlView : LinearLayout { } fun setFullscreen(isFullScreen: Boolean) { - _scaleFactor = 1.0f - onZoom.emit(_scaleFactor) - _translationX = 0f - _translationY = 0f - onPan.emit(_translationX, _translationY) + resetZoomPan() if (isFullScreen) { val c = context diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index a84dcef6..df3a24fb 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt @@ -272,6 +272,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase { _videoView.scaleY = it } + gestureControl.setZoomPanEnabled(_videoView.videoSurfaceView!!) + if(!isInEditMode) { _videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; val player = StatePlayer.instance.getPlayerOrCreate(context); @@ -600,6 +602,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase { } override fun onVideoSizeChanged(videoSize: VideoSize) { + gestureControl.resetZoomPan() _lastSourceFit = null; if(isFullScreen) fillHeight(); @@ -763,4 +766,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase { fun setGestureSoundFactor(soundFactor: Float) { gestureControl.setSoundFactor(soundFactor); } + + override fun onSurfaceSizeChanged(width: Int, height: Int) { + gestureControl.resetZoomPan() + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index d60a3f8e..3179e282 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -104,6 +104,11 @@ abstract class FutoVideoPlayerBase : RelativeLayout { super.onPlaybackSuppressionReasonChanged(playbackSuppressionReason) } + override fun onSurfaceSizeChanged(width: Int, height: Int) { + super.onSurfaceSizeChanged(width, height) + this@FutoVideoPlayerBase.onSurfaceSizeChanged(width, height); + } + override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying); this@FutoVideoPlayerBase.onIsPlayingChanged(isPlaying); @@ -592,6 +597,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout { exoPlayer?.setVolume(volume); } + protected open fun onSurfaceSizeChanged(width: Int, height: Int) { + + } + @Suppress("DEPRECATION") protected open fun onPlayerError(error: PlaybackException) { Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss"); diff --git a/app/src/main/res/drawable/background_primary_border.xml b/app/src/main/res/drawable/background_primary_border.xml new file mode 100644 index 00000000..4b155c58 --- /dev/null +++ b/app/src/main/res/drawable/background_primary_border.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <stroke android:color="#992D63ED" android:width="5dp" /> + <padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/layout/view_gesture_controls.xml b/app/src/main/res/layout/view_gesture_controls.xml index a358a60a..ea175d31 100644 --- a/app/src/main/res/layout/view_gesture_controls.xml +++ b/app/src/main/res/layout/view_gesture_controls.xml @@ -177,4 +177,22 @@ android:textSize="16dp"/> </FrameLayout> + <FrameLayout + android:id="@+id/layout_indicator_fill" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/background_primary_border" + android:visibility="gone" /> + + <FrameLayout + android:id="@+id/layout_indicator_fit" + android:layout_width="100dp" + android:layout_height="100dp" + android:background="@drawable/background_primary_border" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:visibility="gone"/> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file -- GitLab