fix(YouTube): Use correct fade out animation when tapping to dismiss the video overlay (#5670)

This commit is contained in:
LisoUseInAIKyrios
2025-08-17 20:14:44 -04:00
committed by GitHub
parent 3130225d9d
commit 01a04c338c
18 changed files with 291 additions and 45 deletions

View File

@@ -57,11 +57,4 @@ public class PlayerControlsPatch {
private static void fullscreenButtonVisibilityChanged(boolean isVisible) {
// Code added during patching.
}
/**
* Injection point.
*/
public static String getPlayerTopControlsLayoutResourceName(String original) {
return "default";
}
}

View File

@@ -0,0 +1,18 @@
package app.revanced.extension.youtube.patches;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.shared.PlayerControlsVisibility;
@SuppressWarnings("unused")
public class PlayerControlsVisibilityHookPatch {
/**
* Injection point.
*/
public static void setPlayerControlsVisibility(@Nullable Enum<?> youTubePlayerControlsVisibility) {
if (youTubePlayerControlsVisibility == null) return;
PlayerControlsVisibility.setFromString(youTubePlayerControlsVisibility.name());
}
}

View File

@@ -9,7 +9,7 @@ public class SpoofAppVersionPatch {
private static final String SPOOF_APP_VERSION_TARGET = Settings.SPOOF_APP_VERSION_TARGET.get();
/**
* Injection point
* injection point.
*/
public static String getYouTubeVersionOverride(String version) {
if (SPOOF_APP_VERSION_ENABLED) return SPOOF_APP_VERSION_TARGET;

View File

@@ -126,7 +126,7 @@ public final class SeekbarColorPatch {
}
/**
* Injection point
* injection point.
*/
public static boolean useLotteLaunchSplashScreen(boolean original) {
// This method is only used for development purposes to force the old style launch screen.

View File

@@ -0,0 +1,48 @@
package app.revanced.extension.youtube.shared
import app.revanced.extension.shared.Logger
import app.revanced.extension.youtube.Event
/**
* PlayerControls visibility state.
*/
enum class PlayerControlsVisibility {
PLAYER_CONTROLS_VISIBILITY_UNKNOWN,
PLAYER_CONTROLS_VISIBILITY_WILL_HIDE,
PLAYER_CONTROLS_VISIBILITY_HIDDEN,
PLAYER_CONTROLS_VISIBILITY_WILL_SHOW,
PLAYER_CONTROLS_VISIBILITY_SHOWN;
companion object {
private val nameToPlayerControlsVisibility = PlayerControlsVisibility.entries.associateBy { it.name }
@JvmStatic
fun setFromString(enumName: String) {
val newType = nameToPlayerControlsVisibility[enumName]
if (newType == null) {
Logger.printException { "Unknown PlayerControlsVisibility encountered: $enumName" }
} else {
current = newType
}
}
@JvmStatic
var current
get() = currentPlayerControlsVisibility
private set(type) {
if (currentPlayerControlsVisibility != type) {
Logger.printDebug { "Changed to: $type" }
currentPlayerControlsVisibility = type
onChange(type)
}
}
@Volatile // Read/write from different threads.
private var currentPlayerControlsVisibility = PLAYER_CONTROLS_VISIBILITY_UNKNOWN
@JvmStatic
val onChange = Event<PlayerControlsVisibility>()
}
}

View File

@@ -13,7 +13,6 @@ import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Range;
import android.view.Gravity;
import android.view.ViewGroup;
@@ -877,7 +876,7 @@ public class SegmentPlaybackController {
}
/**
* Injection point
* injection point.
*/
@SuppressWarnings("unused")
public static void setSponsorBarRect(Object self) {
@@ -909,7 +908,7 @@ public class SegmentPlaybackController {
}
/**
* Injection point
* injection point.
*/
@SuppressWarnings("unused")
public static void setSponsorBarThickness(int thickness) {

View File

@@ -18,7 +18,7 @@ public class CreateSegmentButton {
}
/**
* injection point
* injection point.
*/
public static void initialize(View controlsView) {
try {
@@ -37,14 +37,21 @@ public class CreateSegmentButton {
}
/**
* Injection point
* injection point.
*/
public static void setVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
/**
* injection point.
*/
public static void setVisibilityImmediate(boolean visible) {
if (instance != null) instance.setVisibilityImmediate(visible);
}
/**
* Injection point
* injection point.
*/
public static void setVisibility(boolean visible, boolean animated) {
if (instance != null) instance.setVisibility(visible, animated);

View File

@@ -20,7 +20,7 @@ public class VotingButton {
}
/**
* injection point
* injection point.
*/
public static void initialize(View controlsView) {
try {
@@ -39,14 +39,21 @@ public class VotingButton {
}
/**
* Injection point
* injection point.
*/
public static void setVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
/**
* injection point.
*/
public static void setVisibilityImmediate(boolean visible) {
if (instance != null) instance.setVisibilityImmediate(visible);
}
/**
* Injection point
* injection point.
*/
public static void setVisibility(boolean visible, boolean animated) {
if (instance != null) instance.setVisibility(visible, animated);

View File

@@ -35,15 +35,22 @@ public class CopyVideoUrlButton {
}
}
/**`
* injection point.
*/
public static void setVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
/**
* injection point
* injection point.
*/
public static void setVisibilityImmediate(boolean visible) {
if (instance != null) instance.setVisibilityImmediate(visible);
}
/**
* injection point
* injection point.
*/
public static void setVisibility(boolean visible, boolean animated) {
if (instance != null) instance.setVisibility(visible, animated);

View File

@@ -36,14 +36,21 @@ public class CopyVideoUrlTimestampButton {
}
/**
* injection point
* injection point.
*/
public static void setVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
/**
* injection point.
*/
public static void setVisibilityImmediate(boolean visible) {
if (instance != null) instance.setVisibilityImmediate(visible);
}
/**
* injection point
* injection point.
*/
public static void setVisibility(boolean visible, boolean animated) {
if (instance != null) instance.setVisibility(visible, animated);

View File

@@ -34,14 +34,21 @@ public class ExternalDownloadButton {
}
/**
* injection point
* injection point.
*/
public static void setVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
/**
* injection point.
*/
public static void setVisibilityImmediate(boolean visible) {
if (instance != null) instance.setVisibilityImmediate(visible);
}
/**
* Injection point
* injection point.
*/
public static void setVisibility(boolean visible, boolean animated) {
if (instance != null) instance.setVisibility(visible, animated);

View File

@@ -69,6 +69,13 @@ public class PlaybackSpeedDialogButton {
}
}
/**
* injection point.
*/
public static void setVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
/**
* Injection point.
*/

View File

@@ -11,6 +11,7 @@ import java.lang.ref.WeakReference;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.shared.PlayerControlsVisibility;
import app.revanced.extension.youtube.shared.PlayerType;
import kotlin.Unit;
@@ -40,8 +41,6 @@ public class PlayerControlButton {
fadeOutAnimation = Utils.getResourceAnimation("fade_out");
fadeOutAnimation.setDuration(fadeOutDuration);
// Animation for the fast fade out after tapping the overlay.
// Currently not used but should be.
fadeOutImmediate = Utils.getResourceAnimation("abc_fade_out");
fadeOutImmediate.setDuration(Utils.getResourceInteger("fade_duration_fast"));
}
@@ -114,6 +113,33 @@ public class PlayerControlButton {
});
}
public void setVisibilityNegatedImmediate() {
if (PlayerControlsVisibility.getCurrent() != PlayerControlsVisibility.PLAYER_CONTROLS_VISIBILITY_HIDDEN) {
return;
}
final boolean buttonEnabled = enabledStatus.buttonEnabled();
if (!buttonEnabled) {
return;
}
View container = containerRef.get();
if (container == null) {
return;
}
isVisible = false;
container.clearAnimation();
container.startAnimation(fadeOutImmediate);
container.setVisibility(View.GONE);
View placeholder = placeHolderRef.get();
if (placeholder != null) {
container.setVisibility(View.VISIBLE);
}
}
public void setVisibilityImmediate(boolean visible) {
if (visible) {
// Fix button flickering, by pushing this call to the back of

View File

@@ -116,6 +116,13 @@ public class VideoQualityDialogButton {
}
}
/**
* injection point.
*/
public static void setVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
/**
* Injection point.
*/

View File

@@ -1536,6 +1536,10 @@ public final class app/revanced/patches/youtube/misc/navigation/NavigationBarHoo
public static final fun setHookNavigationButtonCreated (Lkotlin/jvm/functions/Function1;)V
}
public final class app/revanced/patches/youtube/misc/playercontrols/PlayerControlsOverlayVisibilityPatchKt {
public static final fun getPlayerControlsOverlayVisibilityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatchKt {
public static final fun getAddBottomControl ()Lkotlin/jvm/functions/Function1;
public static final fun getPlayerControlsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;

View File

@@ -2,14 +2,63 @@ package app.revanced.patches.youtube.misc.playercontrols
import app.revanced.patcher.fingerprint
import app.revanced.util.containsLiteralInstruction
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.indexOfFirstInstructionReversed
import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal fun indexOfFocusableInTouchModeInstruction(method: Method) =
method.indexOfFirstInstruction {
getReference<MethodReference>()?.name == "setFocusableInTouchMode"
}
internal fun indexOfTranslationInstruction(method: Method) =
method.indexOfFirstInstructionReversed {
getReference<MethodReference>()?.name == "setTranslationY"
}
internal val playerControlsVisibilityEntityModelFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC)
returns("L")
parameters()
opcodes(
Opcode.IGET,
Opcode.INVOKE_STATIC
)
custom { method, _ ->
method.name == "getPlayerControlsVisibility"
}
}
internal val youtubeControlsOverlayFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL)
returns("V")
parameters()
custom { method, _ ->
indexOfFocusableInTouchModeInstruction(method) >= 0 &&
method.containsLiteralInstruction(inset_overlay_view_layout_id) &&
method.containsLiteralInstruction(scrim_overlay_id)
}
}
internal val motionEventFingerprint = fingerprint {
returns("V")
parameters("Landroid/view/MotionEvent;")
custom { method, _ ->
indexOfTranslationInstruction(method) >= 0
}
}
internal val playerTopControlsInflateFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
parameters()
literal { controlsLayoutStub }
literal { controls_layout_stub_id }
}
internal val playerControlsExtensionHookListenersExistFingerprint = fingerprint {
@@ -35,7 +84,7 @@ internal val playerControlsExtensionHookFingerprint = fingerprint {
internal val playerBottomControlsInflateFingerprint = fingerprint {
returns("Ljava/lang/Object;")
parameters()
literal { bottomUiContainerResourceId }
literal { bottom_ui_container_stub_id }
}
internal val overlayViewInflateFingerprint = fingerprint {
@@ -43,8 +92,8 @@ internal val overlayViewInflateFingerprint = fingerprint {
returns("V")
parameters("Landroid/view/View;")
custom { methodDef, _ ->
methodDef.containsLiteralInstruction(fullscreenButton) &&
methodDef.containsLiteralInstruction(heatseekerViewstub)
methodDef.containsLiteralInstruction(fullscreen_button_id) &&
methodDef.containsLiteralInstruction(heatseeker_viewstub_id)
}
}

View File

@@ -0,0 +1,42 @@
package app.revanced.patches.youtube.misc.playercontrols
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
private const val EXTENSION_PLAYER_CONTROLS_VISIBILITY_HOOK_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/PlayerControlsVisibilityHookPatch;"
val PlayerControlsOverlayVisibilityPatch = bytecodePatch {
dependsOn(sharedExtensionPatch)
execute {
playerControlsVisibilityEntityModelFingerprint.let {
it.method.apply {
val startIndex = it.patternMatch!!.startIndex
val iGetReference = getInstruction<ReferenceInstruction>(startIndex).reference
val staticReference = getInstruction<ReferenceInstruction>(startIndex + 1).reference
it.classDef.methods.find { method -> method.name == "<init>" }?.apply {
val targetIndex = indexOfFirstInstructionOrThrow(Opcode.IPUT_OBJECT)
val targetRegister = getInstruction<TwoRegisterInstruction>(targetIndex).registerA
addInstructions(
targetIndex + 1,
"""
iget v$targetRegister, v$targetRegister, $iGetReference
invoke-static { v$targetRegister }, $staticReference
move-result-object v$targetRegister
invoke-static { v$targetRegister }, $EXTENSION_PLAYER_CONTROLS_VISIBILITY_HOOK_CLASS_DESCRIPTOR->setPlayerControlsVisibility(Ljava/lang/Enum;)V
"""
)
}
}
}
}
}

View File

@@ -1,7 +1,6 @@
package app.revanced.patches.youtube.misc.playercontrols
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
@@ -40,13 +39,17 @@ internal lateinit var addTopControl: (String) -> Unit
lateinit var addBottomControl: (String) -> Unit
private set
internal var bottomUiContainerResourceId = -1L
internal var bottom_ui_container_stub_id = -1L
private set
internal var controlsLayoutStub = -1L
internal var controls_layout_stub_id = -1L
private set
internal var heatseekerViewstub = -1L
internal var heatseeker_viewstub_id = -1L
private set
internal var fullscreenButton = -1L
internal var fullscreen_button_id = -1L
private set
internal var inset_overlay_view_layout_id = -1L
private set
internal var scrim_overlay_id = -1L
private set
val playerControlsResourcePatch = resourcePatch {
@@ -65,10 +68,12 @@ val playerControlsResourcePatch = resourcePatch {
execute {
val targetResourceName = "youtube_controls_bottom_ui_container.xml"
bottomUiContainerResourceId = resourceMappings["id", "bottom_ui_container_stub"]
controlsLayoutStub = resourceMappings["id", "controls_layout_stub"]
heatseekerViewstub = resourceMappings["id", "heatseeker_viewstub"]
fullscreenButton = resourceMappings["id", "fullscreen_button"]
bottom_ui_container_stub_id = resourceMappings["id", "bottom_ui_container_stub"]
controls_layout_stub_id = resourceMappings["id", "controls_layout_stub"]
heatseeker_viewstub_id = resourceMappings["id", "heatseeker_viewstub"]
fullscreen_button_id = resourceMappings["id", "fullscreen_button"]
inset_overlay_view_layout_id = resourceMappings["id", "inset_overlay_view_layout"]
scrim_overlay_id = resourceMappings["id", "scrim_overlay"]
bottomTargetDocument = document("res/layout/$targetResourceName")
@@ -198,6 +203,13 @@ fun injectVisibilityCheckCall(descriptor: String) {
visibilityImmediateInsertIndex++,
"invoke-static { p0 }, $descriptor->setVisibilityImmediate(Z)V",
)
// Patch works without this hook, but it is needed to use the correct fade out animation
// duration when tapping the overlay to dismiss.
visibilityNegatedImmediateMethod.addInstruction(
visibilityNegatedImmediateInsertIndex++,
"invoke-static { }, $descriptor->setVisibilityNegatedImmediate()V",
)
}
internal const val EXTENSION_CLASS_DESCRIPTOR =
@@ -220,12 +232,16 @@ private lateinit var visibilityImmediateCallbacksExistMethod : MutableMethod
private lateinit var visibilityImmediateMethod: MutableMethod
private var visibilityImmediateInsertIndex: Int = 0
private lateinit var visibilityNegatedImmediateMethod: MutableMethod
private var visibilityNegatedImmediateInsertIndex: Int = 0
val playerControlsPatch = bytecodePatch(
description = "Manages the code for the player controls of the YouTube player.",
) {
dependsOn(
playerControlsResourcePatch,
sharedExtensionPatch,
PlayerControlsOverlayVisibilityPatch
)
execute {
@@ -258,7 +274,7 @@ val playerControlsPatch = bytecodePatch(
// Hook the fullscreen close button. Used to fix visibility
// when seeking and other situations.
overlayViewInflateFingerprint.method.apply {
val resourceIndex = indexOfFirstLiteralInstructionReversedOrThrow(fullscreenButton)
val resourceIndex = indexOfFirstLiteralInstructionReversedOrThrow(fullscreen_button_id)
val index = indexOfFirstInstructionOrThrow(resourceIndex) {
opcode == Opcode.CHECK_CAST &&
@@ -277,6 +293,11 @@ val playerControlsPatch = bytecodePatch(
visibilityImmediateCallbacksExistMethod = playerControlsExtensionHookListenersExistFingerprint.method
visibilityImmediateMethod = playerControlsExtensionHookFingerprint.method
motionEventFingerprint.match(youtubeControlsOverlayFingerprint.originalClassDef).method.apply {
visibilityNegatedImmediateMethod = this
visibilityNegatedImmediateInsertIndex = indexOfTranslationInstruction(this) + 1
}
// A/B test for a slightly different bottom overlay controls,
// that uses layout file youtube_video_exploder_controls_bottom_ui_container.xml
// The change to support this is simple and only requires adding buttons to both layout files,
@@ -299,12 +320,9 @@ val playerControlsPatch = bytecodePatch(
val index = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT_OBJECT)
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructions(
addInstruction(
index + 1,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getPlayerTopControlsLayoutResourceName(Ljava/lang/String;)Ljava/lang/String;
move-result-object v$register
""",
"const-string v$register, \"default\""
)
}
}