commit ad7bf747a5d4866f07807dec5caf0444dd41d588 parent 06b4ab52c3eb5234cc74526070642d1a0a8b229a Author: Mugurell <Mugurell@users.noreply.github.com> Date: Wed, 12 Nov 2025 08:07:06 +0000 Bug 1991654 - part 2 - Use a generic EngineViewScrollingBehavior r=android-reviewers,Roger This will allow to easily switch between the current way of scrolling the dynamic toolbar and the new one based on APZ scrolling data. Differential Revision: https://phabricator.services.mozilla.com/D267222 Diffstat:
17 files changed, 328 insertions(+), 290 deletions(-)
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt @@ -37,7 +37,7 @@ import mozilla.components.support.ktx.kotlin.trimmed import mozilla.components.ui.autocomplete.AutocompleteView import mozilla.components.ui.autocomplete.InlineAutocompleteEditText import mozilla.components.ui.autocomplete.OnFilterListener -import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior import kotlin.coroutines.CoroutineContext internal fun ImageView.setTintResource(@ColorRes tintColorResource: Int) { @@ -394,26 +394,26 @@ class BrowserToolbar @JvmOverloads constructor( override fun enableScrolling() { // Behavior can be changed without us knowing. Not safe to use a memoized value. (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.enableScrolling() + (behavior as? EngineViewScrollingBehavior)?.enableScrolling() } } override fun disableScrolling() { // Behavior can be changed without us knowing. Not safe to use a memoized value. (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.disableScrolling() + (behavior as? EngineViewScrollingBehavior)?.disableScrolling() } } override fun expand() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.forceExpand(this@BrowserToolbar) + (behavior as? EngineViewScrollingBehavior)?.forceExpand() } } override fun collapse() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.forceCollapse(this@BrowserToolbar) + (behavior as? EngineViewScrollingBehavior)?.forceCollapse() } } diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt @@ -20,6 +20,7 @@ import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.browser.toolbar.display.DisplayToolbarViews import mozilla.components.browser.toolbar.display.MenuButton import mozilla.components.browser.toolbar.edit.EditToolbar +import mozilla.components.concept.engine.EngineView import mozilla.components.concept.toolbar.AutocompleteDelegate import mozilla.components.concept.toolbar.Toolbar import mozilla.components.concept.toolbar.Toolbar.SiteInfo @@ -29,8 +30,8 @@ import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.whenever +import mozilla.components.ui.widgets.behavior.DependencyGravity.Bottom import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior -import mozilla.components.ui.widgets.behavior.ViewPosition import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals @@ -42,6 +43,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers import org.mockito.Mockito.any +import org.mockito.Mockito.doReturn import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.spy @@ -924,7 +926,9 @@ class BrowserToolbarTest { fun `enable scrolling is forwarded to the toolbar behavior`() { // Seems like real instances are needed for things to be set properly val toolbar = BrowserToolbar(testContext) - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val engineView: EngineView = mock() + doReturn(View(testContext)).`when`(engineView).asView() + val behavior = spy(EngineViewScrollingGesturesBehavior(engineView, toolbar, Bottom)) val params = CoordinatorLayout.LayoutParams(10, 10).apply { this.behavior = behavior } @@ -939,7 +943,9 @@ class BrowserToolbarTest { fun `disable scrolling is forwarded to the toolbar behavior`() { // Seems like real instances are needed for things to be set properly val toolbar = BrowserToolbar(testContext) - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val engineView: EngineView = mock() + doReturn(View(testContext)).`when`(engineView).asView() + val behavior = spy(EngineViewScrollingGesturesBehavior(engineView, toolbar, Bottom)) val params = CoordinatorLayout.LayoutParams(10, 10).apply { this.behavior = behavior } @@ -954,7 +960,9 @@ class BrowserToolbarTest { fun `expand is forwarded to the toolbar behavior`() { // Seems like real instances are needed for things to be set properly val toolbar = BrowserToolbar(testContext) - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val engineView: EngineView = mock() + doReturn(View(testContext)).`when`(engineView).asView() + val behavior = spy(EngineViewScrollingGesturesBehavior(engineView, toolbar, Bottom)) val params = CoordinatorLayout.LayoutParams(10, 10).apply { this.behavior = behavior } @@ -962,14 +970,16 @@ class BrowserToolbarTest { toolbar.expand() - verify(behavior).forceExpand(toolbar) + verify(behavior).forceExpand() } @Test fun `collapse is forwarded to the toolbar behavior`() { // Seems like real instances are needed for things to be set properly val toolbar = BrowserToolbar(testContext) - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val engineView: EngineView = mock() + doReturn(View(testContext)).`when`(engineView).asView() + val behavior = spy(EngineViewScrollingGesturesBehavior(engineView, toolbar, Bottom)) val params = CoordinatorLayout.LayoutParams(10, 10).apply { this.behavior = behavior } @@ -977,7 +987,7 @@ class BrowserToolbarTest { toolbar.collapse() - verify(behavior).forceCollapse(toolbar) + verify(behavior).forceCollapse() } @Test diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/DependencyGravity.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/DependencyGravity.kt @@ -0,0 +1,20 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.ui.widgets.behavior + +/** + * Where the dynamic view dependent on webpage scrolls is placed on the screen. + */ +sealed interface DependencyGravity { + /** + * The view is placed at the top of the screen. + */ + data object Top : DependencyGravity + + /** + * The view is placed at the bottom of the screen. + */ + data object Bottom : DependencyGravity +} diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehavior.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehavior.kt @@ -0,0 +1,61 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.ui.widgets.behavior + +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.coordinatorlayout.widget.CoordinatorLayout +import mozilla.components.concept.engine.EngineView + +/** + * [CoordinatorLayout.Behavior] that will synchronize scrolling between [engineView] and [dependency]. + * + * @param engineView [EngineView] to synchronize scrolling with. + * @param dependency [View] that will be scrolled to match [engineView]. + * @param dependencyGravity whether [dependency] is placed on the top or bottom of the screen. + */ +abstract class EngineViewScrollingBehavior( + engineView: EngineView, + private val dependency: View, + dependencyGravity: DependencyGravity, +) : CoordinatorLayout.Behavior<View>(engineView.asView().context, null) { + var isScrollEnabled = false + @VisibleForTesting set + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + internal var yTranslator = ViewYTranslator(dependencyGravity) + + /** + * Used to expand the dependent View. + */ + fun forceExpand() { + yTranslator.expandWithAnimation(dependency) + } + + /** + * Used to collapse the dependent View. + */ + fun forceCollapse() { + yTranslator.collapseWithAnimation(dependency) + } + + /** + * Allow this view to be animated. + * + * @see disableScrolling + */ + fun enableScrolling() { + isScrollEnabled = true + } + + /** + * Disable scrolling of the view irrespective of the intrinsic checks. + * + * @see enableScrolling + */ + fun disableScrolling() { + isScrollEnabled = false + } +} diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehaviorFactory.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehaviorFactory.kt @@ -0,0 +1,34 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.ui.widgets.behavior + +import android.view.View +import mozilla.components.concept.base.crash.CrashReporting +import mozilla.components.concept.engine.EngineView + +/** + * Factory for [EngineViewScrollingBehavior] instances. + */ +object EngineViewScrollingBehaviorFactory { + /** + * Create a new [EngineViewScrollingBehavior] instance. + * + * @param engineView [EngineView] to synchronize scrolling with. + * @param dependency [View] that will be scrolled to match [engineView]. + * @param dependencyGravity whether [dependency] is placed on the top or bottom of the screen. + * @param crashReporting [CrashReporting] to use for reporting crashes. + */ + fun build( + engineView: EngineView, + dependency: View, + dependencyGravity: DependencyGravity, + crashReporting: CrashReporting? = null, + ) = EngineViewScrollingGesturesBehavior( + engineView = engineView, + dependency = dependency, + dependencyGravity = dependencyGravity, + crashReporting = crashReporting, + ) +} diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingGesturesBehavior.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingGesturesBehavior.kt @@ -4,41 +4,31 @@ package mozilla.components.ui.widgets.behavior -import android.content.Context -import android.util.AttributeSet import android.view.MotionEvent import android.view.View import androidx.annotation.VisibleForTesting import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.ViewCompat +import androidx.core.view.isVisible import mozilla.components.concept.base.crash.CrashReporting import mozilla.components.concept.engine.EngineView -import mozilla.components.support.ktx.android.view.findViewInHierarchy /** - * Where the view is placed on the screen. - */ -enum class ViewPosition { - TOP, - BOTTOM, -} - -/** - * A [CoordinatorLayout.Behavior] implementation to be used when placing [View] at the bottom of the screen. + * A [CoordinatorLayout.Behavior] implementation to be used for moving [dependency] up/down + * depending on scroll events in [engineView]. * - * This is safe to use even if the [View] may be added / removed from a parent layout later - * or if it could have Visibility.GONE set. + * This is safe to use even if [dependency] has it's visibility modified. * * This implementation will: * - Show/Hide the [View] automatically when scrolling vertically. * - Snap the [View] to be hidden or visible when the user stops scrolling. */ class EngineViewScrollingGesturesBehavior( - val context: Context?, - attrs: AttributeSet?, - private val viewPosition: ViewPosition, + private val engineView: EngineView, + private val dependency: View, + dependencyGravity: DependencyGravity, private val crashReporting: CrashReporting? = null, -) : CoordinatorLayout.Behavior<View>(context, attrs) { +) : EngineViewScrollingBehavior(engineView, dependency, dependencyGravity) { // This implementation is heavily based on this blog article: // https://android.jlelse.eu/scroll-your-bottom-navigation-view-away-with-10-lines-of-code-346f1ed40e9e @@ -48,21 +38,6 @@ class EngineViewScrollingGesturesBehavior( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var startedScroll = false - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var isScrollEnabled = false - - /** - * Reference to [EngineView] used to check user's [android.view.MotionEvent]s. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var engineView: EngineView? = null - - /** - * Reference to the actual [View] that we'll animate. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var dynamicScrollView: View? = null - /** * Depending on how user's touch was consumed by EngineView / current website, * @@ -75,18 +50,13 @@ class EngineViewScrollingGesturesBehavior( */ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal val shouldScroll: Boolean - get() = engineView?.getInputResultDetail()?.let { + get() = engineView.getInputResultDetail().let { (it.canScrollToBottom() || it.canScrollToTop()) && isScrollEnabled - } ?: false + } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var gesturesDetector: BrowserGestureDetector = createGestureDetector() - @VisibleForTesting - internal var yTranslator: ViewYTranslator = createYTranslationStrategy() - - private fun createYTranslationStrategy() = ViewYTranslator(viewPosition) - override fun onStartNestedScroll( coordinatorLayout: CoordinatorLayout, child: View, @@ -94,12 +64,9 @@ class EngineViewScrollingGesturesBehavior( target: View, axes: Int, type: Int, - ): Boolean { - return if (dynamicScrollView != null) { - startNestedScroll(axes, type) - } else { - return false // not interested in subsequent scroll events - } + ): Boolean = when (dependency.isVisible) { + true -> startNestedScroll(axes, type) + false -> false // not interested in subsequent scroll events } override fun onStopNestedScroll( @@ -108,7 +75,7 @@ class EngineViewScrollingGesturesBehavior( target: View, type: Int, ) { - if (dynamicScrollView != null) { + if (dependency.isVisible) { stopNestedScroll(type, child) } } @@ -118,86 +85,30 @@ class EngineViewScrollingGesturesBehavior( child: View, ev: MotionEvent, ): Boolean { - if (dynamicScrollView != null) { + if (dependency.isVisible) { gesturesDetector.handleTouchEvent(ev) } - return false // allow events to be passed to below listeners - } - - override fun onLayoutChild( - parent: CoordinatorLayout, - child: View, - layoutDirection: Int, - ): Boolean { - dynamicScrollView = child - engineView = parent.findViewInHierarchy { it is EngineView } as? EngineView - - return super.onLayoutChild(parent, child, layoutDirection) - } - - /** - * Used to expand the [View] - */ - fun forceExpand(view: View) { - yTranslator.expandWithAnimation(view) - } - - /** - * Used to collapse the [View] - */ - fun forceCollapse(view: View) { - yTranslator.collapseWithAnimation(view) - } - - /** - * Allow this view to be animated. - * - * @see disableScrolling - */ - fun enableScrolling() { - isScrollEnabled = true - } - /** - * Disable scrolling of the view irrespective of the intrinsic checks. - * - * @see enableScrolling - */ - fun disableScrolling() { - isScrollEnabled = false + return false // allow events to be passed to below listeners } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun tryToScrollVertically(distance: Float) { - dynamicScrollView?.let { view -> - if (shouldScroll && startedScroll) { - yTranslator.translate(view, distance) - } + if (shouldScroll && startedScroll) { + yTranslator.translate(dependency, distance) } } - /** - * Helper function to ease testing. - * (Re)Initializes the [BrowserGestureDetector] in a new context. - * - * Useful in spied behaviors, to ensure callbacks are of the spy and not of the initially created object - * if the passed in argument is the result of [createGestureDetector]. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun initGesturesDetector(detector: BrowserGestureDetector) { - gesturesDetector = detector - } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun createGestureDetector() = BrowserGestureDetector( - context!!, + engineView.asView().context, BrowserGestureDetector.GesturesListener( onVerticalScroll = ::tryToScrollVertically, onScaleBegin = { // Scale shouldn't animate the view but a small y translation is still possible // because of a previous scroll. Try to be swift about such an in progress animation. - yTranslator.snapImmediately(dynamicScrollView) + yTranslator.snapImmediately(dependency) }, ), crashReporting = crashReporting, diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/ViewYTranslator.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/ViewYTranslator.kt @@ -6,6 +6,7 @@ package mozilla.components.ui.widgets.behavior import android.view.View import androidx.annotation.VisibleForTesting +import mozilla.components.ui.widgets.behavior.DependencyGravity.Top /** * Helper class with methods for translating on the Y axis a top / bottom [View]. @@ -15,7 +16,7 @@ import androidx.annotation.VisibleForTesting * - if place at the bottom it will be Y translated between 0 and [View.getHeight] * - if place at the top it will be Y translated between -[View.getHeight] and 0 */ -class ViewYTranslator(viewPosition: ViewPosition) { +class ViewYTranslator(viewPosition: DependencyGravity) { @VisibleForTesting internal var strategy = getTranslationStrategy(viewPosition) @@ -71,8 +72,8 @@ class ViewYTranslator(viewPosition: ViewPosition) { } @VisibleForTesting - internal fun getTranslationStrategy(viewPosition: ViewPosition): ViewYTranslationStrategy { - return if (viewPosition == ViewPosition.TOP) { + internal fun getTranslationStrategy(dependencyGravity: DependencyGravity): ViewYTranslationStrategy { + return if (dependencyGravity is Top) { TopViewBehaviorStrategy() } else { BottomViewBehaviorStrategy() diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/BrowserGestureDetectorTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/BrowserGestureDetectorTest.kt @@ -67,7 +67,7 @@ class BrowserGestureDetectorTest { } @Test - fun `Detector's handleTouchEvent returns false if the event was not handled`() { + fun `Detector's onTouch returns false if the event was not handled`() { val detector = BrowserGestureDetector(testContext, mock()) val unhandledEvent = TestUtils.getMotionEvent(ACTION_DOWN) @@ -81,7 +81,7 @@ class BrowserGestureDetectorTest { } @Test - fun `Detector's handleTouchEvent returns true if the event was handled`() { + fun `Detector's onTouch returns true if the event was handled`() { val detector = BrowserGestureDetector(testContext, mock()) val downEvent = TestUtils.getMotionEvent(ACTION_DOWN) val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 0f, previousEvent = downEvent) diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingGesturesBehaviorTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingGesturesBehaviorTest.kt @@ -10,8 +10,8 @@ import android.view.MotionEvent.ACTION_DOWN import android.view.MotionEvent.ACTION_MOVE import android.view.View import android.widget.FrameLayout -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.ViewCompat +import androidx.core.view.isVisible import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.flow.flowOf import mozilla.components.concept.engine.EngineSession @@ -21,7 +21,7 @@ import mozilla.components.concept.engine.selection.SelectionActionDelegate import mozilla.components.support.test.any import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext -import org.junit.Assert.assertEquals +import mozilla.components.ui.widgets.behavior.DependencyGravity.Bottom import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -32,16 +32,16 @@ import org.mockito.Mockito.doReturn import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` @RunWith(AndroidJUnit4::class) class EngineViewScrollingGesturesBehaviorTest { @Test fun `onStartNestedScroll should attempt scrolling only if browserToolbar is valid`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val browserToolbar: View = mock() + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), browserToolbar, Bottom)) doReturn(true).`when`(behavior).shouldScroll - behavior.dynamicScrollView = null + doReturn(View.GONE).`when`(browserToolbar).isVisible var acceptsNestedScroll = behavior.onStartNestedScroll( coordinatorLayout = mock(), child = mock(), @@ -53,7 +53,7 @@ class EngineViewScrollingGesturesBehaviorTest { assertFalse(acceptsNestedScroll) verify(behavior, never()).startNestedScroll(anyInt(), anyInt()) - behavior.dynamicScrollView = mock() + doReturn(View.VISIBLE).`when`(browserToolbar).isVisible acceptsNestedScroll = behavior.onStartNestedScroll( coordinatorLayout = mock(), child = mock(), @@ -68,7 +68,7 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `startNestedScroll should cancel an ongoing snap animation`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), mock(), Bottom)) val yTranslator: ViewYTranslator = mock() behavior.yTranslator = yTranslator doReturn(true).`when`(behavior).shouldScroll @@ -84,7 +84,7 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `startNestedScroll should not accept nested scrolls on the horizontal axis`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), mock(), Bottom)) doReturn(true).`when`(behavior).shouldScroll var acceptsNestedScroll = behavior.startNestedScroll( @@ -102,14 +102,13 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `GIVEN a gesture that doesn't scroll the toolbar WHEN startNestedScroll THEN nested scroll is not accepted`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) - val engineView: EngineView = mock() + val engineView = spy(createDummyEngineView()) + val behavior = spy(EngineViewScrollingGesturesBehavior(engineView, mock(), Bottom)) val inputResultDetail: InputResultDetail = mock() val yTranslator: ViewYTranslator = mock() behavior.yTranslator = yTranslator doReturn(false).`when`(behavior).shouldScroll doReturn(true).`when`(inputResultDetail).isTouchUnhandled() - behavior.engineView = engineView doReturn(inputResultDetail).`when`(engineView).getInputResultDetail() val acceptsNestedScroll = behavior.startNestedScroll( @@ -122,8 +121,7 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `Behavior should not accept nested scrolls on the horizontal axis`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) - behavior.dynamicScrollView = mock() + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), mock(), Bottom)) doReturn(true).`when`(behavior).shouldScroll var acceptsNestedScroll = behavior.onStartNestedScroll( @@ -149,15 +147,14 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `Behavior should delegate the onStartNestedScroll logic`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) - val view: View = mock() - behavior.dynamicScrollView = view + val dependency: View = mock() + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom)) val inputType = ViewCompat.TYPE_TOUCH val axes = ViewCompat.SCROLL_AXIS_VERTICAL behavior.onStartNestedScroll( coordinatorLayout = mock(), - child = view, + child = dependency, directTargetChild = mock(), target = mock(), axes = axes, @@ -169,9 +166,10 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `onStopNestedScroll should attempt stopping nested scrolling only if browserToolbar is valid`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val dependency: View = mock() + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom)) - behavior.dynamicScrollView = null + doReturn(View.GONE).`when`(dependency).visibility behavior.onStopNestedScroll( coordinatorLayout = mock(), child = mock(), @@ -180,7 +178,7 @@ class EngineViewScrollingGesturesBehaviorTest { ) verify(behavior, never()).stopNestedScroll(anyInt(), any()) - behavior.dynamicScrollView = mock() + doReturn(View.VISIBLE).`when`(dependency).visibility behavior.onStopNestedScroll( coordinatorLayout = mock(), child = mock(), @@ -192,26 +190,25 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `Behavior should delegate the onStopNestedScroll logic`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val dependency: View = mock() + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom)) val inputType = ViewCompat.TYPE_TOUCH - val view: View = mock() - behavior.dynamicScrollView = null + doReturn(View.GONE).`when`(dependency).visibility behavior.onStopNestedScroll( coordinatorLayout = mock(), - child = view, + child = dependency, target = mock(), type = inputType, ) - verify(behavior, never()).stopNestedScroll(inputType, view) + verify(behavior, never()).stopNestedScroll(inputType, dependency) } @Test fun `stopNestedScroll will snap toolbar up if toolbar is more than 50 percent visible`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), mock(), Bottom)) val yTranslator: ViewYTranslator = mock() behavior.yTranslator = yTranslator - behavior.dynamicScrollView = mock() doReturn(true).`when`(behavior).shouldScroll val child = mock<View>() @@ -239,19 +236,18 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `stopNestedScroll will snap toolbar down if toolbar is less than 50 percent visible`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val dependency: View = mock() + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom)) doReturn(true).`when`(behavior).shouldScroll val yTranslator: ViewYTranslator = mock() behavior.yTranslator = yTranslator - val child = mock<View>() - behavior.dynamicScrollView = child - doReturn(100).`when`(child).height - doReturn(90f).`when`(child).translationY + doReturn(100).`when`(dependency).height + doReturn(90f).`when`(dependency).translationY behavior.onStartNestedScroll( coordinatorLayout = mock(), - child = child, + child = dependency, directTargetChild = mock(), target = mock(), axes = ViewCompat.SCROLL_AXIS_VERTICAL, @@ -263,15 +259,16 @@ class EngineViewScrollingGesturesBehaviorTest { verify(yTranslator, never()).expandWithAnimation(any()) verify(yTranslator, never()).collapseWithAnimation(any()) - behavior.stopNestedScroll(0, child) + behavior.stopNestedScroll(0, dependency) - verify(yTranslator).snapWithAnimation(child) + verify(yTranslator).snapWithAnimation(dependency) } @Test fun `onStopNestedScroll should snap the toolbar only if browserToolbar is valid`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) - behavior.dynamicScrollView = null + val dependency: View = mock() + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom)) + doReturn(View.GONE).`when`(dependency).visibility behavior.onStopNestedScroll( coordinatorLayout = mock(), @@ -285,10 +282,9 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `Behavior will intercept MotionEvents and pass them to the custom gesture detector`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val behavior = EngineViewScrollingGesturesBehavior(createDummyEngineView(), mock(), Bottom) val gestureDetector: BrowserGestureDetector = mock() - behavior.initGesturesDetector(gestureDetector) - behavior.dynamicScrollView = mock() + behavior.gesturesDetector = gestureDetector val downEvent = TestUtils.getMotionEvent(ACTION_DOWN) behavior.onInterceptTouchEvent(mock(), mock(), downEvent) @@ -298,9 +294,11 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `Behavior should only dispatch MotionEvents to the gesture detector only if browserToolbar is valid`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val dependency: View = mock() + doReturn(View.GONE).`when`(dependency).isVisible + val behavior = EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom) val gestureDetector: BrowserGestureDetector = mock() - behavior.initGesturesDetector(gestureDetector) + behavior.gesturesDetector = gestureDetector val downEvent = TestUtils.getMotionEvent(ACTION_DOWN) behavior.onInterceptTouchEvent(mock(), mock(), downEvent) @@ -310,35 +308,29 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `Behavior will apply translation to toolbar only for vertical scrolls`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) - behavior.initGesturesDetector(behavior.createGestureDetector()) - val child = spy(View(testContext, null, 0)) - behavior.dynamicScrollView = child + val dependency: View = mock() + val engineView = spy(createDummyEngineView()) + val behavior = EngineViewScrollingGesturesBehavior(engineView, dependency, Bottom) + val yTranslator: ViewYTranslator = mock() + behavior.isScrollEnabled = true + behavior.startedScroll = true + behavior.yTranslator = yTranslator + val validInputResultDetail: InputResultDetail = mock() + doReturn(true).`when`(validInputResultDetail).canScrollToBottom() + doReturn(validInputResultDetail).`when`(engineView).getInputResultDetail() val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f) val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 100f, downEvent) behavior.onInterceptTouchEvent(mock(), mock(), downEvent) behavior.onInterceptTouchEvent(mock(), mock(), moveEvent) - verify(behavior).tryToScrollVertically(-100f) - } - - @Test - fun `GIVEN a null InputResultDetail from the EngineView WHEN shouldScroll is called THEN it returns false`() { - val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) - behavior.engineView = null - assertFalse(behavior.shouldScroll) - behavior.engineView = mock() - `when`(behavior.engineView!!.getInputResultDetail()).thenReturn(null) - - assertFalse(behavior.shouldScroll) + verify(yTranslator).translate(dependency, -100f) } @Test fun `GIVEN an InputResultDetail with the right values and scroll enabled WHEN shouldScroll is called THEN it returns true`() { - val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) - val engineView: EngineView = mock() - behavior.engineView = engineView + val engineView = spy(createDummyEngineView()) + val behavior = EngineViewScrollingGesturesBehavior(engineView, mock(), Bottom) behavior.isScrollEnabled = true val validInputResultDetail: InputResultDetail = mock() doReturn(validInputResultDetail).`when`(engineView).getInputResultDetail() @@ -358,8 +350,8 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `GIVEN an InputResultDetail with the right values but with scroll disabled WHEN shouldScroll is called THEN it returns false`() { - val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) - behavior.engineView = mock() + val engineView = spy(createDummyEngineView()) + val behavior = EngineViewScrollingGesturesBehavior(engineView, mock(), Bottom) behavior.isScrollEnabled = false val validInputResultDetail: InputResultDetail = mock() doReturn(true).`when`(validInputResultDetail).canScrollToBottom() @@ -370,8 +362,7 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `GIVEN scroll enabled but EngineView cannot scroll to bottom WHEN shouldScroll is called THEN it returns false`() { - val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) - behavior.engineView = mock() + val behavior = EngineViewScrollingGesturesBehavior(createDummyEngineView(), mock(), Bottom) behavior.isScrollEnabled = true val validInputResultDetail: InputResultDetail = mock() doReturn(false).`when`(validInputResultDetail).canScrollToBottom() @@ -382,8 +373,7 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `GIVEN scroll enabled but EngineView cannot scroll to top WHEN shouldScroll is called THEN it returns false`() { - val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) - behavior.engineView = mock() + val behavior = EngineViewScrollingGesturesBehavior(createDummyEngineView(), mock(), Bottom) behavior.isScrollEnabled = true val validInputResultDetail: InputResultDetail = mock() doReturn(true).`when`(validInputResultDetail).canScrollToBottom() @@ -394,29 +384,28 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `Behavior will vertically scroll nested scroll started and EngineView handled the event`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val dependency = spy(View(testContext, null, 0)) + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom)) val yTranslator: ViewYTranslator = mock() behavior.yTranslator = yTranslator doReturn(true).`when`(behavior).shouldScroll - val child = spy(View(testContext, null, 0)) - behavior.dynamicScrollView = child - doReturn(100).`when`(child).height - doReturn(0f).`when`(child).translationY + doReturn(100).`when`(dependency).height + doReturn(0f).`when`(dependency).translationY behavior.startedScroll = true behavior.tryToScrollVertically(25f) - verify(yTranslator).translate(child, 25f) + verify(yTranslator).translate(dependency, 25f) } @Test fun `Behavior will not scroll vertically if startedScroll is false`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val dependency = spy(View(testContext, null, 0)) + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom)) val yTranslator: ViewYTranslator = mock() behavior.yTranslator = yTranslator doReturn(true).`when`(behavior).shouldScroll val child = spy(View(testContext, null, 0)) - behavior.dynamicScrollView = child doReturn(100).`when`(child).height doReturn(0f).`when`(child).translationY behavior.startedScroll = false @@ -428,12 +417,12 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `Behavior will not scroll vertically if EngineView did not handled the event`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val dependency = spy(View(testContext, null, 0)) + val behavior = spy(EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom)) val yTranslator: ViewYTranslator = mock() behavior.yTranslator = yTranslator doReturn(false).`when`(behavior).shouldScroll val child = spy(View(testContext, null, 0)) - behavior.dynamicScrollView = child doReturn(100).`when`(child).height doReturn(0f).`when`(child).translationY @@ -444,43 +433,43 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `forceExpand should delegate the translator`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val dependency: View = mock() + val behavior = EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom) val yTranslator: ViewYTranslator = mock() behavior.yTranslator = yTranslator - val view: View = mock() - behavior.forceExpand(view) + behavior.forceExpand() - verify(yTranslator).expandWithAnimation(view) + verify(yTranslator).expandWithAnimation(dependency) } @Test fun `forceCollapse should delegate the translator`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val dependency: View = mock() + val behavior = EngineViewScrollingGesturesBehavior(createDummyEngineView(), dependency, Bottom) val yTranslator: ViewYTranslator = mock() behavior.yTranslator = yTranslator - val view: View = mock() - behavior.forceCollapse(view) + behavior.forceCollapse() - verify(yTranslator).collapseWithAnimation(view) + verify(yTranslator).collapseWithAnimation(dependency) } @Test fun `Behavior will not forceExpand when scrolling up and !shouldScroll if the touch was not yet handled in the browser`() { - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val dependency: View = mock() + val engineView = spy(createDummyEngineView()) + val behavior = EngineViewScrollingGesturesBehavior(engineView, dependency, Bottom) val yTranslator: ViewYTranslator = mock() + behavior.isScrollEnabled = true + behavior.startedScroll = true behavior.yTranslator = yTranslator - behavior.initGesturesDetector(behavior.createGestureDetector()) - val view: View = spy(View(testContext, null, 0)) - behavior.dynamicScrollView = view - val engineView: EngineView = mock() - behavior.engineView = engineView - val handledTouchInput = InputResultDetail.newInstance() - doReturn(handledTouchInput).`when`(engineView).getInputResultDetail() + val validInputResultDetail: InputResultDetail = mock() + doReturn(true).`when`(validInputResultDetail).canScrollToBottom() + doReturn(validInputResultDetail).`when`(engineView).getInputResultDetail() - doReturn(100).`when`(view).height - doReturn(100f).`when`(view).translationY + doReturn(100).`when`(dependency).height + doReturn(100f).`when`(dependency).translationY val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f) val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 30f, downEvent) @@ -488,29 +477,13 @@ class EngineViewScrollingGesturesBehaviorTest { behavior.onInterceptTouchEvent(mock(), mock(), downEvent) behavior.onInterceptTouchEvent(mock(), mock(), moveEvent) - verify(behavior).tryToScrollVertically(-30f) - verify(yTranslator, never()).forceExpandIfNotAlready(view, -30f) - } - - @Test - fun `onLayoutChild initializes browserToolbar and engineView`() { - val view = View(testContext) - val engineView = createDummyEngineView(testContext).asView() - val container = CoordinatorLayout(testContext).apply { - addView(View(testContext)) - addView(engineView) - } - val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) - - behavior.onLayoutChild(container, view, View.LAYOUT_DIRECTION_LTR) - - assertEquals(view, behavior.dynamicScrollView) - assertEquals(engineView, behavior.engineView) + verify(yTranslator).translate(dependency, -30f) + verify(yTranslator, never()).forceExpandIfNotAlready(dependency, -30f) } @Test fun `enableScrolling sets isScrollEnabled to true`() { - val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) + val behavior = EngineViewScrollingGesturesBehavior(createDummyEngineView(), mock(), Bottom) assertFalse(behavior.isScrollEnabled) behavior.enableScrolling() @@ -520,7 +493,7 @@ class EngineViewScrollingGesturesBehaviorTest { @Test fun `disableScrolling sets isScrollEnabled to false`() { - val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) + val behavior = EngineViewScrollingGesturesBehavior(createDummyEngineView(), mock(), Bottom) behavior.isScrollEnabled = true assertTrue(behavior.isScrollEnabled) @@ -529,7 +502,7 @@ class EngineViewScrollingGesturesBehaviorTest { assertFalse(behavior.isScrollEnabled) } - private fun createDummyEngineView(context: Context): EngineView = DummyEngineView(context) + private fun createDummyEngineView(): EngineView = DummyEngineView(testContext) open class DummyEngineView(context: Context) : FrameLayout(context), EngineView { override val verticalScrollPosition = flowOf(0f) @@ -546,5 +519,6 @@ class EngineViewScrollingGesturesBehaviorTest { listener: androidx.core.view.OnApplyWindowInsetsListener?, ) {} override fun removeWindowInsetsListener(key: String) {} + override fun asView() = View(context) } } diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/ViewYTranslatorTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/ViewYTranslatorTest.kt @@ -7,6 +7,8 @@ package mozilla.components.ui.widgets.behavior import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import mozilla.components.support.test.mock +import mozilla.components.ui.widgets.behavior.DependencyGravity.Bottom +import mozilla.components.ui.widgets.behavior.DependencyGravity.Top import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith @@ -16,21 +18,21 @@ import org.mockito.Mockito.verify class ViewYTranslatorTest { @Test fun `yTranslator should use BottomToolbarBehaviorStrategy for bottom placed toolbars`() { - val yTranslator = ViewYTranslator(ViewPosition.BOTTOM) + val yTranslator = ViewYTranslator(Bottom) assertTrue(yTranslator.strategy is BottomViewBehaviorStrategy) } @Test fun `yTranslator should use TopToolbarBehaviorStrategy for top placed toolbars`() { - val yTranslator = ViewYTranslator(ViewPosition.TOP) + val yTranslator = ViewYTranslator(Top) assertTrue(yTranslator.strategy is TopViewBehaviorStrategy) } @Test fun `yTranslator should delegate it's strategy for snapWithAnimation`() { - val yTranslator = ViewYTranslator(ViewPosition.BOTTOM) + val yTranslator = ViewYTranslator(Bottom) val strategy: ViewYTranslationStrategy = mock() yTranslator.strategy = strategy val view: View = mock() @@ -42,7 +44,7 @@ class ViewYTranslatorTest { @Test fun `yTranslator should delegate it's strategy for expandWithAnimation`() { - val yTranslator = ViewYTranslator(ViewPosition.BOTTOM) + val yTranslator = ViewYTranslator(Bottom) val strategy: ViewYTranslationStrategy = mock() yTranslator.strategy = strategy val view: View = mock() @@ -54,7 +56,7 @@ class ViewYTranslatorTest { @Test fun `yTranslator should delegate it's strategy for collapseWithAnimation`() { - val yTranslator = ViewYTranslator(ViewPosition.BOTTOM) + val yTranslator = ViewYTranslator(Bottom) val strategy: ViewYTranslationStrategy = mock() yTranslator.strategy = strategy val view: View = mock() @@ -66,7 +68,7 @@ class ViewYTranslatorTest { @Test fun `yTranslator should delegate it's strategy for forceExpandIfNotAlready`() { - val yTranslator = ViewYTranslator(ViewPosition.BOTTOM) + val yTranslator = ViewYTranslator(Bottom) val strategy: ViewYTranslationStrategy = mock() yTranslator.strategy = strategy val view: View = mock() @@ -78,7 +80,7 @@ class ViewYTranslatorTest { @Test fun `yTranslator should delegate it's strategy for translate`() { - val yTranslator = ViewYTranslator(ViewPosition.BOTTOM) + val yTranslator = ViewYTranslator(Bottom) val strategy: ViewYTranslationStrategy = mock() yTranslator.strategy = strategy val view: View = mock() @@ -90,7 +92,7 @@ class ViewYTranslatorTest { @Test fun `yTranslator should delegate it's strategy for cancelInProgressTranslation`() { - val yTranslator = ViewYTranslator(ViewPosition.BOTTOM) + val yTranslator = ViewYTranslator(Bottom) val strategy: ViewYTranslationStrategy = mock() yTranslator.strategy = strategy @@ -101,7 +103,7 @@ class ViewYTranslatorTest { @Test fun `yTranslator should delegate it's strategy for snapImmediately`() { - val yTranslator = ViewYTranslator(ViewPosition.BOTTOM) + val yTranslator = ViewYTranslator(Bottom) val strategy: ViewYTranslationStrategy = mock() yTranslator.strategy = strategy val view: View = mock() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BottomToolbarContainerView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BottomToolbarContainerView.kt @@ -12,9 +12,12 @@ import android.widget.LinearLayout import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ComposeView import androidx.coordinatorlayout.widget.CoordinatorLayout +import mozilla.components.concept.engine.EngineView import mozilla.components.concept.toolbar.ScrollableToolbar -import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior -import mozilla.components.ui.widgets.behavior.ViewPosition +import mozilla.components.support.ktx.android.view.findViewInHierarchy +import mozilla.components.ui.widgets.behavior.DependencyGravity.Bottom +import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehaviorFactory import org.mozilla.fenix.R /** @@ -49,8 +52,13 @@ class BottomToolbarContainerView( CoordinatorLayout.LayoutParams.WRAP_CONTENT, ).apply { gravity = Gravity.BOTTOM - if (hideOnScroll) { - behavior = EngineViewScrollingGesturesBehavior(parent.context, null, ViewPosition.BOTTOM) + val engineView = parent.findViewInHierarchy { it is EngineView } as? EngineView + if (hideOnScroll && engineView != null) { + behavior = EngineViewScrollingBehaviorFactory.build( + engineView = engineView, + dependency = toolbarContainerView, + dependencyGravity = Bottom, + ) } } @@ -78,25 +86,25 @@ class ToolbarContainerView @JvmOverloads constructor( ) : LinearLayout(context, attrs, defStyleAttr), ScrollableToolbar { override fun enableScrolling() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.enableScrolling() + (behavior as? EngineViewScrollingBehavior)?.enableScrolling() } } override fun disableScrolling() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.disableScrolling() + (behavior as? EngineViewScrollingBehavior)?.disableScrolling() } } override fun expand() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.forceExpand(this@ToolbarContainerView) + (behavior as? EngineViewScrollingBehavior)?.forceExpand() } } override fun collapse() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.forceCollapse(this@ToolbarContainerView) + (behavior as? EngineViewScrollingBehavior)?.forceCollapse() } } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserNavigationBar.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserNavigationBar.kt @@ -49,7 +49,7 @@ class BrowserNavigationBar( private val hideWhenKeyboardShown: Boolean, customTabSession: CustomTabSessionState? = null, ) : FenixBrowserToolbarView( - context = context, + parent = container, settings = settings, customTabSession = customTabSession, ) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarComposable.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarComposable.kt @@ -69,7 +69,7 @@ class BrowserToolbarComposable( private val searchSuggestionsContent: @Composable (Modifier) -> Unit, private val navigationBarContent: (@Composable () -> Unit)?, ) : FenixBrowserToolbarView( - context = activity, + parent = container, settings = settings, customTabSession = customTabSession, ) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt @@ -57,7 +57,7 @@ class BrowserToolbarView( private val lifecycleOwner: LifecycleOwner, private val tabStripContent: @Composable () -> Unit, ) : FenixBrowserToolbarView( - context = context, + parent = container, settings = settings, customTabSession = customTabSession, ) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/FenixBrowserToolbarView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/FenixBrowserToolbarView.kt @@ -4,27 +4,31 @@ package org.mozilla.fenix.components.toolbar -import android.content.Context import android.view.View +import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible import mozilla.components.browser.state.state.CustomTabSessionState import mozilla.components.browser.state.state.ExternalAppType +import mozilla.components.concept.engine.EngineView import mozilla.components.concept.toolbar.ScrollableToolbar -import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior -import mozilla.components.ui.widgets.behavior.ViewPosition +import mozilla.components.support.ktx.android.view.findViewInHierarchy +import mozilla.components.ui.widgets.behavior.DependencyGravity.Bottom +import mozilla.components.ui.widgets.behavior.DependencyGravity.Top +import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehaviorFactory import org.mozilla.fenix.utils.Settings /** * Base class for the browser toolbar implementations. * - * @param context [Context] used for various system interactions. + * @param parent The [ViewGroup] into which the toolbar will be added. * @param settings [Settings] object to get the toolbar position and other settings. * @param customTabSession [CustomTabSessionState] if the toolbar is shown in a custom tab. */ abstract class FenixBrowserToolbarView( - private val context: Context, + private val parent: ViewGroup, private val settings: Settings, private val customTabSession: CustomTabSessionState?, ) : ScrollableToolbar { @@ -49,7 +53,7 @@ abstract class FenixBrowserToolbarView( } (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.forceExpand(layout) + (behavior as? EngineViewScrollingBehavior)?.forceExpand() } } @@ -60,19 +64,19 @@ abstract class FenixBrowserToolbarView( } (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.forceCollapse(layout) + (behavior as? EngineViewScrollingBehavior)?.forceCollapse() } } override fun enableScrolling() { (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.enableScrolling() + (behavior as? EngineViewScrollingBehavior)?.enableScrolling() } } override fun disableScrolling() { (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { - (behavior as? EngineViewScrollingGesturesBehavior)?.disableScrolling() + (behavior as? EngineViewScrollingBehavior)?.disableScrolling() } } @@ -103,7 +107,7 @@ abstract class FenixBrowserToolbarView( if (settings.isDynamicToolbarEnabled && !settings.shouldUseFixedTopToolbar ) { - setDynamicToolbarBehavior(ViewPosition.BOTTOM) + setDynamicToolbarBehavior(true) } else { expandToolbarAndMakeItFixed() } @@ -115,7 +119,7 @@ abstract class FenixBrowserToolbarView( ) { expandToolbarAndMakeItFixed() } else { - setDynamicToolbarBehavior(ViewPosition.TOP) + setDynamicToolbarBehavior(false) } } } @@ -130,9 +134,18 @@ abstract class FenixBrowserToolbarView( } @VisibleForTesting - internal fun setDynamicToolbarBehavior(toolbarPosition: ViewPosition) { - (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { - behavior = EngineViewScrollingGesturesBehavior(layout.context, null, toolbarPosition) + internal fun setDynamicToolbarBehavior(isToolbarAtBottom: Boolean) { + (parent.findViewInHierarchy { it is EngineView } as? EngineView)?.let { engineView -> + (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { + behavior = EngineViewScrollingBehaviorFactory.build( + engineView = engineView, + dependency = layout, + dependencyGravity = when (isToolbarAtBottom) { + true -> Bottom + false -> Top + }, + ) + } } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserHomeToolbarViewTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserHomeToolbarViewTest.kt @@ -12,8 +12,10 @@ import io.mockk.mockk import io.mockk.spyk import io.mockk.verify import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.concept.engine.EngineView import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.support.test.robolectric.testContext +import mozilla.components.ui.widgets.behavior.DependencyGravity.Bottom import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -25,7 +27,6 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.utils.Settings import org.robolectric.RobolectricTestRunner -import mozilla.components.ui.widgets.behavior.ViewPosition as MozacToolbarPosition @RunWith(RobolectricTestRunner::class) class BrowserHomeToolbarViewTest { @@ -59,8 +60,11 @@ class BrowserHomeToolbarViewTest { tabStripContent = {}, ) + val engineView: EngineView = mockk { + every { asView() } returns View(testContext) + } toolbarView.toolbar = toolbar - behavior = spyk(EngineViewScrollingGesturesBehavior(testContext, null, MozacToolbarPosition.BOTTOM)) + behavior = spyk(EngineViewScrollingGesturesBehavior(engineView, toolbar, Bottom)) (toolbarView.layout.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior } @@ -74,7 +78,7 @@ class BrowserHomeToolbarViewTest { toolbarViewSpy.setToolbarBehavior(settings.toolbarPosition, false) - verify { toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) } + verify { toolbarViewSpy.setDynamicToolbarBehavior(true) } } @Test @@ -100,7 +104,7 @@ class BrowserHomeToolbarViewTest { toolbarViewSpy.setToolbarBehavior(settings.toolbarPosition, false) - verify { toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) } + verify { toolbarViewSpy.setDynamicToolbarBehavior(true) } } @Test @@ -128,7 +132,7 @@ class BrowserHomeToolbarViewTest { toolbarViewSpy.setToolbarBehavior(settings.toolbarPosition, false) - verify { toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) } + verify { toolbarViewSpy.setDynamicToolbarBehavior(true) } } @Test @@ -154,7 +158,7 @@ class BrowserHomeToolbarViewTest { toolbarViewSpy.setToolbarBehavior(settings.toolbarPosition, false) - verify { toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) } + verify { toolbarViewSpy.setDynamicToolbarBehavior(true) } } @Test @@ -231,7 +235,7 @@ class BrowserHomeToolbarViewTest { val toolbarViewSpy = spyk(toolbarView) (toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior = null - toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) + toolbarViewSpy.setDynamicToolbarBehavior(true) assertNotNull((toolbarView.layout.layoutParams as CoordinatorLayout.LayoutParams).behavior) } @@ -241,7 +245,7 @@ class BrowserHomeToolbarViewTest { val toolbarViewSpy = spyk(toolbarView) (toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior = null - toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.TOP) + toolbarViewSpy.setDynamicToolbarBehavior(false) assertNotNull((toolbarView.layout.layoutParams as CoordinatorLayout.LayoutParams).behavior) } @@ -266,7 +270,7 @@ class BrowserHomeToolbarViewTest { toolbarViewSpy.expand() - verify { behavior.forceExpand(toolbarView.layout) } + verify { behavior.forceExpand() } } @Test @@ -289,7 +293,7 @@ class BrowserHomeToolbarViewTest { toolbarViewSpy.collapse() - verify { behavior.forceCollapse(toolbarView.layout) } + verify { behavior.forceCollapse() } } @Test diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserToolbar.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserToolbar.kt @@ -9,10 +9,10 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.concept.engine.EngineView +import mozilla.components.ui.widgets.behavior.DependencyGravity.Top import mozilla.components.ui.widgets.behavior.EngineViewClippingBehavior import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior import org.mozilla.focus.R -import mozilla.components.ui.widgets.behavior.ViewPosition as browserToolbarPosition private const val BOTTOM_TOOLBAR_HEIGHT = 0 @@ -40,9 +40,9 @@ fun BrowserToolbar.disableDynamicBehavior(engineView: EngineView) { */ fun BrowserToolbar.enableDynamicBehavior(context: Context, engineView: EngineView) { (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = EngineViewScrollingGesturesBehavior( - context, - null, - browserToolbarPosition.TOP, + engineView = engineView, + dependency = this, + dependencyGravity = Top, ) val toolbarHeight = context.resources.getDimension(R.dimen.browser_toolbar_height).toInt()