Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 91 additions & 5 deletions app/src/main/java/org/schabi/newpipe/player/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()));
}
Expand Down Expand Up @@ -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;

Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ///////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 -> {
Expand All @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/res/layout/player.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@
android:layout_alignBottom="@+id/surfaceView"
android:background="@android:color/black" />

<TextView
android:id="@+id/pinchZoomIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="48dp"
android:background="@drawable/background_rectangle_black_transparent"
android:paddingHorizontal="6dp"
android:paddingVertical="3dp"
android:textColor="@android:color/white"
android:textSize="11sp"
android:visibility="gone"
tools:text="1.5×"
tools:visibility="visible" />

<com.google.android.exoplayer2.ui.SubtitleView
android:id="@+id/subtitleView"
android:layout_width="match_parent"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-tr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,8 @@
<string name="decoder_init_failure">Kod çözücü başlatma başarısız oldu</string>
<string name="unable_to_decode_summary">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.</string>
<string name="settings_category_gestures_summary">Jest Ayarları</string>
<string name="resize_pinch">Sıkıştır</string>
<string name="pinch_to_zoom_selected">Yakınlaştırmak için sıkıştırın.</string>
<string name="settings_category_bullet_comments_summary">Kayan Yorum Ayarları</string>
<string name="settings_category_backup_title">Yedekleme</string>
<string name="settings_category_advanced_title">Gelişmiş</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/settings_keys.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<string name="volume_gesture_control_key">volume_gesture_control</string>
<string name="brightness_gesture_control_key">brightness_gesture_control</string>
<string name="fullscreen_gesture_control_key">fullscreen_gesture_control</string>
<string name="pinch_to_zoom_key">pinch_to_zoom</string>
<string name="swipe_seek_gesture_control_key">swipe_seek_gesture_control</string>
<string name="playback_speed_gesture_control_key">playback_speed_gesture_control</string>
<string name="resume_on_audio_focus_gain_key">resume_on_audio_focus_gain</string>
Expand Down
Loading