[Android] Fix for Open in new tab animation (#36974)

Resolves https://github.com/brave/brave-browser/issues/56076

must be reverted for `cr150`
This commit is contained in:
AlexeyBarabash
2026-06-05 00:46:50 +03:00
committed by GitHub
parent e66c446dd5
commit 759269a49a
6 changed files with 267 additions and 0 deletions
+1
View File
@@ -100,6 +100,7 @@ brave_java_sources = [
"../../brave/android/java/org/chromium/chrome/browser/browsing_data/BraveClearBrowsingDataFragment.java",
"../../brave/android/java/org/chromium/chrome/browser/browsing_data/BraveShortcutsUtils.java",
"../../brave/android/java/org/chromium/chrome/browser/compositor/layouts/BraveToolbarSwipeLayout.java",
"../../brave/android/java/org/chromium/chrome/browser/compositor/layouts/phone/BraveNewTabAnimationLayout.java",
"../../brave/android/java/org/chromium/chrome/browser/contextmenu/BraveChromeContextMenuPopulator.java",
"../../brave/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java",
"../../brave/android/java/org/chromium/chrome/browser/crash/BravePureJavaExceptionReporter.java",
@@ -0,0 +1,88 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
package org.chromium.chrome.browser.compositor.layouts.phone;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import org.chromium.build.annotations.NullMarked;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.toolbar.bottom.BottomToolbarConfiguration;
/**
* Brave helper for {@link NewTabAnimationLayout}.
*
* <p>When Brave's bottom toolbar is active the top tab-switcher button may be GONE (empty rect) or
* visible alongside the bottom one. In either case the bottom toolbar's tab-switcher button is the
* correct animation destination. {@link #compute} encapsulates the fallback logic and returns a
* {@link BraveTabSwitcherState} that callers unpack into the four variables that drive {@code
* tabCreatedInBackground}: {@code tabSwitcherRect}, {@code tabSwitcherButtonIsVisible}, {@code
* toolbarHeight}, and {@code isTopToolbar}.
*
* <p>Both classes share the same Java package so no import is needed in the patch.
*/
@NullMarked
public class BraveNewTabAnimationLayout {
/** Immutable snapshot of the four variables that drive the background-tab animation. */
public static class BraveTabSwitcherState {
public final Rect rect;
public final boolean isVisible;
public final int toolbarHeight;
public final boolean isTopToolbar;
BraveTabSwitcherState(
Rect rect, boolean isVisible, int toolbarHeight, boolean isTopToolbar) {
this.rect = rect;
this.isVisible = isVisible;
this.toolbarHeight = toolbarHeight;
this.isTopToolbar = isTopToolbar;
}
}
/**
* Returns a {@link BraveTabSwitcherState} for the background-tab animation.
*
* <p>When Brave's bottom toolbar is active and its tab-switcher button is visible, overrides
* the supplied originals so the animation targets the bottom button. Otherwise returns the
* originals unchanged (e.g. split-screen small-window mode where the bottom toolbar is hidden,
* or when Brave bottom controls are not enabled).
*
* @param animationHostView The host view used to traverse to the root view.
* @param origRect Original {@code tabSwitcherRect} from the top toolbar button.
* @param origIsVisible Original {@code tabSwitcherButtonIsVisible}.
* @param origToolbarHeight Original {@code toolbarHeight} (top-toolbar Y).
* @param origIsTopToolbar Original {@code isTopToolbar} flag.
*/
public static BraveTabSwitcherState compute(
ViewGroup animationHostView,
Rect origRect,
boolean origIsVisible,
int origToolbarHeight,
boolean origIsTopToolbar) {
if (BottomToolbarConfiguration.isBraveBottomControlsEnabled()) {
View bottomToolbar = animationHostView.getRootView().findViewById(R.id.bottom_toolbar);
if (bottomToolbar != null) {
View bottomButton = bottomToolbar.findViewById(R.id.bottom_tab_switcher_button);
if (bottomButton != null) {
Rect braveRect = new Rect();
if (bottomButton.getGlobalVisibleRect(braveRect)) {
return new BraveTabSwitcherState(
braveRect,
/* isVisible= */ true,
/* toolbarHeight= */ braveRect.top,
/* isTopToolbar= */ false);
}
}
}
}
return new BraveTabSwitcherState(
origRect, origIsVisible, origToolbarHeight, origIsTopToolbar);
}
private BraveNewTabAnimationLayout() {}
}
+8
View File
@@ -10,6 +10,14 @@ import("//third_party/jni_zero/jni_zero.gni")
testonly = true
if (is_android) {
robolectric_library(
"brave_junit_tests_org.chromium.chrome.browser.compositor") {
resources_package = "org.chromium.chrome"
sources = [ "src/org/chromium/chrome/browser/compositor/layouts/phone/BraveNewTabAnimationLayoutUnitTest.java" ]
deps = [ "//chrome/android/junit:chrome_junit_tests_helper" ]
}
robolectric_library(
"brave_junit_tests_org.chromium.chrome.browser.homepage") {
sources = [
@@ -0,0 +1,157 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
package org.chromium.chrome.browser.compositor.layouts.phone;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.chromium.base.BravePreferenceKeys;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.compositor.layouts.phone.BraveNewTabAnimationLayout.BraveTabSwitcherState;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
/** Unit tests for {@link BraveNewTabAnimationLayout}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class BraveNewTabAnimationLayoutUnitTest {
private static final int ORIG_TOOLBAR_HEIGHT = 150;
private static final int BOTTOM_LEFT = 700;
private static final int BOTTOM_TOP = 1900;
private static final int BOTTOM_RIGHT = 850;
private static final int BOTTOM_BOTTOM = 2000;
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock private ViewGroup mAnimationHostView;
@Mock private View mRootView;
@Mock private View mBottomToolbar;
@Mock private View mBottomTabSwitcherButton;
private final Rect mOrigRect = new Rect(0, 0, 0, 0);
@Before
public void setUp() {
// Wire getRootView() -> mRootView -> bottom_toolbar -> bottom_tab_switcher_button.
lenient().when(mAnimationHostView.getRootView()).thenReturn(mRootView);
lenient().when(mRootView.findViewById(R.id.bottom_toolbar)).thenReturn(mBottomToolbar);
lenient()
.when(mBottomToolbar.findViewById(R.id.bottom_tab_switcher_button))
.thenReturn(mBottomTabSwitcherButton);
lenient()
.when(mBottomTabSwitcherButton.getGlobalVisibleRect(any(Rect.class)))
.thenAnswer(
invocation -> {
Rect r = invocation.getArgument(0);
r.set(BOTTOM_LEFT, BOTTOM_TOP, BOTTOM_RIGHT, BOTTOM_BOTTOM);
return true;
});
}
@After
public void tearDown() {
ChromeSharedPreferences.getInstance()
.writeBoolean(BravePreferenceKeys.BRAVE_BOTTOM_TOOLBAR_SET_KEY, false);
}
private void enableBraveBottomControls() {
ChromeSharedPreferences.getInstance()
.writeBoolean(BravePreferenceKeys.BRAVE_BOTTOM_TOOLBAR_SET_KEY, true);
ChromeSharedPreferences.getInstance()
.writeBoolean(BravePreferenceKeys.BRAVE_BOTTOM_TOOLBAR_ENABLED_KEY, true);
}
private void disableBraveBottomControls() {
ChromeSharedPreferences.getInstance()
.writeBoolean(BravePreferenceKeys.BRAVE_BOTTOM_TOOLBAR_SET_KEY, true);
ChromeSharedPreferences.getInstance()
.writeBoolean(BravePreferenceKeys.BRAVE_BOTTOM_TOOLBAR_ENABLED_KEY, false);
}
@Test
@SmallTest
public void testCompute_bottomEnabled_bottomButtonVisible_returnsBottomState() {
enableBraveBottomControls();
BraveTabSwitcherState s =
BraveNewTabAnimationLayout.compute(
mAnimationHostView,
mOrigRect,
/* origIsVisible= */ false,
ORIG_TOOLBAR_HEIGHT,
/* origIsTopToolbar= */ true);
assertFalse("Rect should not be empty", s.rect.isEmpty());
assertEquals(BOTTOM_LEFT, s.rect.left);
assertEquals(BOTTOM_TOP, s.rect.top);
assertEquals(BOTTOM_RIGHT, s.rect.right);
assertEquals(BOTTOM_BOTTOM, s.rect.bottom);
assertTrue("isVisible should be true", s.isVisible);
assertEquals("toolbarHeight should equal bottom button top", BOTTOM_TOP, s.toolbarHeight);
assertFalse("isTopToolbar should be false", s.isTopToolbar);
}
@Test
@SmallTest
public void testCompute_bottomDisabled_returnsOriginals() {
// Bottom address bar is enabled
disableBraveBottomControls();
BraveTabSwitcherState s =
BraveNewTabAnimationLayout.compute(
mAnimationHostView,
mOrigRect,
/* origIsVisible= */ false,
ORIG_TOOLBAR_HEIGHT,
/* origIsTopToolbar= */ true);
assertTrue("Rect should be the original empty rect", s.rect.isEmpty());
assertFalse("isVisible should be the original false", s.isVisible);
assertEquals(ORIG_TOOLBAR_HEIGHT, s.toolbarHeight);
assertTrue("isTopToolbar should be the original true", s.isTopToolbar);
}
@Test
@SmallTest
public void testCompute_bottomEnabled_bottomButtonNotVisible_returnsOriginals() {
// Split-screen / small-window case
enableBraveBottomControls();
when(mBottomTabSwitcherButton.getGlobalVisibleRect(any(Rect.class))).thenReturn(false);
BraveTabSwitcherState s =
BraveNewTabAnimationLayout.compute(
mAnimationHostView,
mOrigRect,
/* origIsVisible= */ false,
ORIG_TOOLBAR_HEIGHT,
/* origIsTopToolbar= */ true);
assertTrue("Rect should be the original when bottom button not visible", s.rect.isEmpty());
assertEquals(ORIG_TOOLBAR_HEIGHT, s.toolbarHeight);
assertTrue(
"isTopToolbar should be original when bottom button not visible", s.isTopToolbar);
}
}
@@ -0,0 +1,12 @@
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/phone/NewTabAnimationLayout.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/phone/NewTabAnimationLayout.java
index 9433bb6de83f05fe8c9b269a43119418a325578b..37f981bd9f86da89cbef7c89b285bbdf307e6f20 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/phone/NewTabAnimationLayout.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/phone/NewTabAnimationLayout.java
@@ -700,6 +700,7 @@ public class NewTabAnimationLayout extends Layout {
isRegularNtp || ToolbarPositionController.shouldShowToolbarOnTop(animationTab);
int toolbarHeight = toolbarPosition[1] + getTopInsetIfNeeded(animationTab);
+ BraveNewTabAnimationLayout.BraveTabSwitcherState braveState = BraveNewTabAnimationLayout.compute(mAnimationHostView, tabSwitcherRect, tabSwitcherButtonIsVisible, toolbarHeight, isTopToolbar); tabSwitcherRect = braveState.rect; tabSwitcherButtonIsVisible = braveState.isVisible; toolbarHeight = braveState.toolbarHeight; isTopToolbar = braveState.isTopToolbar;
Rect compositorViewRect = new Rect();
mCompositorViewHolder.getGlobalVisibleRect(compositorViewRect);
+1
View File
@@ -1687,6 +1687,7 @@ if (is_android) {
"//brave/android/junit:brave_junit_tests_org.chromium.chrome.browser.app",
"//brave/android/junit:brave_junit_tests_org.chromium.chrome.browser.autofill",
"//brave/android/junit:brave_junit_tests_org.chromium.chrome.browser.brave_origin",
"//brave/android/junit:brave_junit_tests_org.chromium.chrome.browser.compositor",
"//brave/android/junit:brave_junit_tests_org.chromium.chrome.browser.custom_search_engines",
"//brave/android/junit:brave_junit_tests_org.chromium.chrome.browser.homepage",
"//brave/android/junit:brave_junit_tests_org.chromium.chrome.browser.ntp",