diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 484185b7c..87547780d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -1132,8 +1132,11 @@ public void smoothStopPlayer() {
//region Player type specific setup
private void initVideoPlayer() {
- // restore last resize mode
- setResizeMode(PlayerHelper.retrieveResizeModeFromPrefs(this));
+ // Pinch zoom owns video scaling while enabled; otherwise restore the regular display mode.
+ setResizeMode(PlayerHelper.isPinchToZoomEnabled(context)
+ ? AspectRatioFrameLayout.RESIZE_MODE_FIT
+ : PlayerHelper.retrieveResizeModeFromPrefs(this));
+ binding.surfaceView.resetPinchScale();
binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
}
@@ -3749,6 +3752,13 @@ private void onMetadataChanged(@NonNull final StreamInfo info) {
Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
}
+ // Zoom belongs to the current video, matching the transient behavior of the official app.
+ resetPinchZoom();
+ if (PlayerHelper.isPinchToZoomEnabled(context)) {
+ forcedAspectRatio = 0.0f;
+ setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
+ }
+
// a forced aspect ratio is a per-video correction, don't carry it over to the next one;
// it temporarily forced the resize mode to Fit, so restore the persisted resize mode
if (forcedAspectRatio > 0) {
@@ -4858,7 +4868,9 @@ private void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMo
* resize mode, since selecting an aspect ratio is what the user sees applied.
*/
private void updateDisplayModeButtonText() {
- binding.resizeTextView.setText(forcedAspectRatio > 0
+ binding.resizeTextView.setText(PlayerHelper.isPinchToZoomEnabled(context)
+ ? getContext().getString(R.string.resize_pinch)
+ : forcedAspectRatio > 0
? PlayerHelper.aspectRatioNameOf(forcedAspectRatio)
: PlayerHelper.resizeTypeOf(context, binding.surfaceView.getResizeMode()));
}
@@ -4893,7 +4905,8 @@ private void buildDisplayModeMenu() {
displayModePopupMenu.setOnDismissListener(this);
// a forced aspect ratio takes precedence: when active, no resize mode is the "current" one
- final boolean ratioActive = forcedAspectRatio > 0;
+ final boolean pinchActive = PlayerHelper.isPinchToZoomEnabled(context);
+ final boolean ratioActive = forcedAspectRatio > 0 && !pinchActive;
final int currentResizeMode = binding.surfaceView.getResizeMode();
MenuItem activeItem = null;
@@ -4908,12 +4921,23 @@ private void buildDisplayModeMenu() {
onResizeModeSelected(resizeMode);
return true;
});
- if (!ratioActive && resizeMode == currentResizeMode) {
+ if (!ratioActive && !pinchActive && resizeMode == currentResizeMode) {
activeItem = resizeItem;
}
order++;
}
+ final MenuItem pinchItem = menu.add(POPUP_MENU_ID_DISPLAY_MODE, order, order,
+ R.string.resize_pinch);
+ pinchItem.setOnMenuItemClickListener(menuItem -> {
+ onPinchModeSelected();
+ return true;
+ });
+ if (pinchActive) {
+ activeItem = pinchItem;
+ }
+ order++;
+
for (int i = 0; i < PlayerHelper.ASPECT_RATIO_VALUES.length; i++) {
final float ratio = PlayerHelper.ASPECT_RATIO_VALUES[i];
final MenuItem ratioItem = menu.add(POPUP_MENU_ID_ASPECT_RATIO, order, order,
@@ -4947,6 +4971,8 @@ private void buildDisplayModeMenu() {
}
private void onResizeModeSelected(@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
+ PlayerHelper.setPinchToZoomEnabled(context, false);
+ resetPinchZoom();
// a resize mode supersedes any forced aspect ratio, which would otherwise have no effect
forcedAspectRatio = 0.0f;
if (videoNaturalAspectRatio > 0) {
@@ -4957,6 +4983,8 @@ private void onResizeModeSelected(@AspectRatioFrameLayout.ResizeMode final int r
}
private void setForcedAspectRatio(final float aspectRatio) {
+ PlayerHelper.setPinchToZoomEnabled(context, false);
+ resetPinchZoom();
forcedAspectRatio = aspectRatio;
// a forced aspect ratio is only meaningful with Fit; this resize mode change is per-video
// and is intentionally not persisted, so the saved resize mode is restored on the next video
@@ -4968,6 +4996,62 @@ private void setForcedAspectRatio(final float aspectRatio) {
}
}
+ public boolean isPinchToZoomEnabled() {
+ return isFullscreen && PlayerHelper.isPinchToZoomEnabled(context);
+ }
+
+ private void onPinchModeSelected() {
+ forcedAspectRatio = 0.0f;
+ PlayerHelper.setPinchToZoomEnabled(context, true);
+ if (videoNaturalAspectRatio > 0.0f) {
+ binding.surfaceView.setAspectRatio(videoNaturalAspectRatio);
+ }
+ setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
+ resetPinchZoom();
+ updateDisplayModeButtonText();
+ Toast.makeText(context, R.string.pinch_to_zoom_selected, Toast.LENGTH_SHORT).show();
+ }
+
+ private void resetPinchZoom() {
+ binding.surfaceView.resetPinchScale();
+ binding.pinchZoomIndicator.animate().cancel();
+ binding.pinchZoomIndicator.setVisibility(View.GONE);
+ }
+
+ public void onPinchZoomStart(final float focusX, final float focusY) {
+ forcedAspectRatio = 0.0f;
+ if (videoNaturalAspectRatio > 0.0f) {
+ binding.surfaceView.setAspectRatio(videoNaturalAspectRatio);
+ }
+ setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
+ binding.surfaceView.beginPinchGesture(
+ focusX - binding.surfaceView.getLeft(),
+ focusY - binding.surfaceView.getTop());
+ binding.pinchZoomIndicator.animate().cancel();
+ binding.pinchZoomIndicator.setAlpha(1.0f);
+ binding.pinchZoomIndicator.setText(String.format(Locale.US, "%.1f×",
+ binding.surfaceView.getPinchScale()));
+ binding.pinchZoomIndicator.setVisibility(View.VISIBLE);
+ }
+
+ public void onPinchZoom(final float scaleFactor, final float focusX, final float focusY) {
+ if (!Float.isFinite(scaleFactor)) {
+ return;
+ }
+ binding.surfaceView.setPinchScale(
+ binding.surfaceView.getPinchScale() * scaleFactor,
+ focusX - binding.surfaceView.getLeft(),
+ focusY - binding.surfaceView.getTop());
+ binding.pinchZoomIndicator.setText(String.format(Locale.US, "%.1f×",
+ binding.surfaceView.getPinchScale()));
+ }
+
+ public void onPinchZoomEnd() {
+ binding.pinchZoomIndicator.animate().cancel();
+ binding.pinchZoomIndicator.animate().alpha(0.0f).setStartDelay(250L).setDuration(180L)
+ .withEndAction(() -> binding.pinchZoomIndicator.setVisibility(View.GONE)).start();
+ }
+
private void openCustomAspectRatioDialog() {
final AppCompatActivity activity = getParentActivity();
if (activity == null) {
@@ -5032,6 +5116,8 @@ public void toggleFullscreen() {
}
isFullscreen = !isFullscreen;
+ // Pinch zoom is fullscreen-only and never survives either direction of the transition.
+ resetPinchZoom();
if (!isFullscreen) {
// Apply window insets because Android will not do it when orientation changes
// from landscape to portrait (open vertical video to reproduce)
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 081d640dd..d24237fbc 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
@@ -28,6 +28,29 @@ abstract class BasePlayerGestureListener(
protected val service: Service
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
+ private val scaleGestureDetector = ScaleGestureDetector(
+ service,
+ object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
+ override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
+ if (!player.isPinchToZoomEnabled || player.popupPlayerSelected()) return false
+ isPinchingInMain = true
+ suppressMainGestureUntilUp = true
+ player.onPinchZoomStart(detector.focusX, detector.focusY)
+ return true
+ }
+
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
+ player.onPinchZoom(detector.scaleFactor, detector.focusX, detector.focusY)
+ return true
+ }
+
+ override fun onScaleEnd(detector: ScaleGestureDetector) {
+ player.onPinchZoomEnd()
+ isPinchingInMain = false
+ }
+ }
+ )
+
// ///////////////////////////////////////////////////////////////////
// Abstract methods for VIDEO and POPUP
// ///////////////////////////////////////////////////////////////////
@@ -59,6 +82,8 @@ abstract class BasePlayerGestureListener(
private var initialPopupY: Int = -1
private var isMovingInMain = false
+ private var isPinchingInMain = false
+ private var suppressMainGestureUntilUp = false
private var isMovingInPopup = false
private var isResizing = false
@@ -86,7 +111,26 @@ abstract class BasePlayerGestureListener(
private var velocityTracker: VelocityTracker? = null
private fun onTouchInMain(v: View, event: MotionEvent): Boolean {
- player.gestureDetector.onTouchEvent(event)
+ if (player.isPinchToZoomEnabled &&
+ event.actionMasked == MotionEvent.ACTION_POINTER_DOWN
+ ) {
+ // GestureDetector does not receive multi-pointer events below, so explicitly cancel
+ // its pending long-press callback before it can enable speed-up during a pinch.
+ val cancelEvent = MotionEvent.obtain(event)
+ cancelEvent.action = MotionEvent.ACTION_CANCEL
+ player.gestureDetector.onTouchEvent(cancelEvent)
+ cancelEvent.recycle()
+ if (player.longPressSpeedingEnabled) {
+ player.playbackSpeed /= player.longPressSpeedingFactor
+ player.longPressSpeedingEnabled = false
+ }
+ }
+ if (player.isPinchToZoomEnabled) {
+ scaleGestureDetector.onTouchEvent(event)
+ }
+ if (!isPinchingInMain && !suppressMainGestureUntilUp && event.pointerCount == 1) {
+ player.gestureDetector.onTouchEvent(event)
+ }
when (event.action) {
MotionEvent.ACTION_DOWN -> {
@@ -95,6 +139,7 @@ abstract class BasePlayerGestureListener(
velocityTracker?.addMovement(event)
}
MotionEvent.ACTION_MOVE -> {
+ if (isPinchingInMain) return true
velocityTracker?.addMovement(event)
velocityTracker?.computeCurrentVelocity(1000)
val yVelocity = velocityTracker?.yVelocity ?: 0f
@@ -111,6 +156,15 @@ abstract class BasePlayerGestureListener(
velocityTracker?.recycle()
velocityTracker = null
+ if (isPinchingInMain) {
+ player.onPinchZoomEnd()
+ isPinchingInMain = false
+ suppressMainGestureUntilUp = false
+ return true
+ }
+
+ suppressMainGestureUntilUp = false
+
if (isMovingInMain) {
isMovingInMain = false
onScrollEnd(PlayerService.PlayerType.VIDEO, event)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index e70d3f327..cf45140f7 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -329,6 +329,17 @@ public static boolean isFullscreenGestureEnabled(@NonNull final Context context)
.getBoolean(context.getString(R.string.fullscreen_gesture_control_key), true);
}
+ public static boolean isPinchToZoomEnabled(@NonNull final Context context) {
+ return getPreferences(context)
+ .getBoolean(context.getString(R.string.pinch_to_zoom_key), false);
+ }
+
+ public static void setPinchToZoomEnabled(@NonNull final Context context,
+ final boolean enabled) {
+ getPreferences(context).edit()
+ .putBoolean(context.getString(R.string.pinch_to_zoom_key), enabled).apply();
+ }
+
public static boolean isSwipeSeekGestureEnabled(@NonNull final Context context) {
return getPreferences(context)
.getBoolean(context.getString(R.string.swipe_seek_gesture_control_key), true);
diff --git a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java
index 7c7651045..14a21117d 100644
--- a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java
+++ b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java
@@ -17,6 +17,11 @@ public class ExpandableSurfaceView extends SurfaceView {
private float videoAspectRatio = 0.0f;
private float scaleX = 1.0f;
private float scaleY = 1.0f;
+ private float pinchScale = 1.0f;
+ private float pinchTranslationX = 0.0f;
+ private float pinchTranslationY = 0.0f;
+ private float lastPinchFocusX = Float.NaN;
+ private float lastPinchFocusY = Float.NaN;
public ExpandableSurfaceView(final Context context, final AttributeSet attrs) {
super(context, attrs);
@@ -70,9 +75,18 @@ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec
@Override
protected void onLayout(final boolean changed,
final int left, final int top, final int right, final int bottom) {
- // Defensive: never push a non-finite scale into the view (it throws and takes down layout).
- setScaleX(Float.isFinite(scaleX) ? scaleX : 1.0f);
- setScaleY(Float.isFinite(scaleY) ? scaleY : 1.0f);
+ applyScaleAndTranslation();
+ }
+
+ private void applyScaleAndTranslation() {
+ final float safePinchScale = Float.isFinite(pinchScale) ? pinchScale : 1.0f;
+ final boolean pinchActive = safePinchScale > 1.0f;
+ setPivotX(pinchActive ? 0.0f : getWidth() / 2.0f);
+ setPivotY(pinchActive ? 0.0f : getHeight() / 2.0f);
+ setScaleX((Float.isFinite(scaleX) ? scaleX : 1.0f) * safePinchScale);
+ setScaleY((Float.isFinite(scaleY) ? scaleY : 1.0f) * safePinchScale);
+ setTranslationX(pinchActive ? pinchTranslationX : 0.0f);
+ setTranslationY(pinchActive ? pinchTranslationY : 0.0f);
}
/**
@@ -102,6 +116,47 @@ public int getResizeMode() {
return resizeMode;
}
+ public void beginPinchGesture(final float focusX, final float focusY) {
+ lastPinchFocusX = focusX;
+ lastPinchFocusY = focusY;
+ }
+
+ public void setPinchScale(final float newScale, final float focusX, final float focusY) {
+ final float oldScale = pinchScale;
+ pinchScale = Math.max(1.0f, Math.min(newScale, 8.0f));
+ final float scaleChange = pinchScale / oldScale;
+
+ if (Float.isFinite(lastPinchFocusX) && Float.isFinite(lastPinchFocusY)) {
+ // Keep the content that was under the fingers anchored while also following their
+ // midpoint. This avoids the inverted, jumpy motion caused by changing View pivots.
+ pinchTranslationX = focusX
+ - (lastPinchFocusX - pinchTranslationX) * scaleChange;
+ pinchTranslationY = focusY
+ - (lastPinchFocusY - pinchTranslationY) * scaleChange;
+ }
+ lastPinchFocusX = focusX;
+ lastPinchFocusY = focusY;
+
+ pinchTranslationX = Math.max(getWidth() * (1.0f - pinchScale),
+ Math.min(0.0f, pinchTranslationX));
+ pinchTranslationY = Math.max(getHeight() * (1.0f - pinchScale),
+ Math.min(0.0f, pinchTranslationY));
+ applyScaleAndTranslation();
+ }
+
+ public float getPinchScale() {
+ return pinchScale;
+ }
+
+ public void resetPinchScale() {
+ pinchScale = 1.0f;
+ pinchTranslationX = 0.0f;
+ pinchTranslationY = 0.0f;
+ lastPinchFocusX = Float.NaN;
+ lastPinchFocusY = Float.NaN;
+ applyScaleAndTranslation();
+ }
+
public void setAspectRatio(final float aspectRatio) {
// A 0x0 / not-yet-known video gives NaN (or Infinity) here; keep it as "no ratio" (0) so the
// measure path skips scaling instead of pushing NaN into setScaleX, which throws
diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml
index 772a8fbd6..92863070e 100644
--- a/app/src/main/res/layout/player.xml
+++ b/app/src/main/res/layout/player.xml
@@ -21,6 +21,22 @@
android:layout_alignBottom="@+id/surfaceView"
android:background="@android:color/black" />
+
+
Kod çözücü başlatma başarısız oldu
Cihazınız gerekli donanım kod çözücülerinden yoksun, bu yüzden bu videoyu PipePipe\'ta oynatamazsınız.\nLütfen indirin ve VLC\'de oynatın.
Jest Ayarları
+Sıkıştır
+Yakınlaştırmak için sıkıştırın.
Kayan Yorum Ayarları
Yedekleme
Gelişmiş
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index 67359f930..760de6d83 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -23,6 +23,7 @@
volume_gesture_control
brightness_gesture_control
fullscreen_gesture_control
+ pinch_to_zoom
swipe_seek_gesture_control
playback_speed_gesture_control
resume_on_audio_focus_gain
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e3ee94dc9..99b21d4e6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -940,6 +940,8 @@
Decoder initialization failed
Your device lacks the required hardware decoders, so you cannot play this video in PipePipe.\nPlease download and play it in VLC.
Gestures
+Pinch
+Pinch to zoom.
Bullet Comment Settings
Backup
Advanced
@@ -1083,4 +1085,4 @@
Notifications when YouTube requires a playback request to wait
Waiting for YouTube
YouTube forces us to wait %1$d seconds before continuing playback
-
\ No newline at end of file
+