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