mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-21 09:49:14 +00:00
Bug 1917818 - part 1 - Synchronize showing browser's bottom toolbar on top of the keyboard r=android-reviewers,sfamisa
Previously when the keyboard would be showing / hiding the entire screen will first resize to the end height after which the keyboard will be animated. This meant just one reflow for the engine view but a choppy layout animation. With the new changes we'll ensure the bottom toolbar is animated in sync with the keyboard and the engine view is resized only once - before hiding the keyboard to ensure that it will then nicely reveal the web content - after showing the full keyboard to ensure it gradually covered web content with no UI issue Differential Revision: https://phabricator.services.mozilla.com/D223585
This commit is contained in:
parent
a5dba92c22
commit
5c92bf47ef
@ -23,9 +23,15 @@ import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||
*
|
||||
* @param targetView The view which will be shown on top of the keyboard while this is animated to be
|
||||
* showing or to be hidden.
|
||||
* @param onIMEAnimationStarted Callback for when the IME animation starts.
|
||||
* It will inform whether the keyboard is showing or hiding and the height of the keyboard.
|
||||
* @param onIMEAnimationFinished Callback for when the IME animation finishes.
|
||||
* It will inform whether the keyboard is showing or hiding and the height of the keyboard.
|
||||
*/
|
||||
class ImeInsetsSynchronizer private constructor(
|
||||
private val targetView: View,
|
||||
private val onIMEAnimationStarted: (Boolean, Int) -> Unit,
|
||||
private val onIMEAnimationFinished: (Boolean, Int) -> Unit,
|
||||
) : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE),
|
||||
OnApplyWindowInsetsListener {
|
||||
|
||||
@ -50,7 +56,15 @@ class ImeInsetsSynchronizer private constructor(
|
||||
view.updateBottomMargin(
|
||||
calculateBottomMargin(
|
||||
windowInsets.keyboardInsets.bottom,
|
||||
windowInsets.navigationBarInsetHeight,
|
||||
getNavbarHeight(),
|
||||
),
|
||||
)
|
||||
|
||||
onIMEAnimationFinished(
|
||||
isKeyboardShowingUp,
|
||||
calculateBottomMargin(
|
||||
windowInsets.keyboardInsets.bottom,
|
||||
getNavbarHeight(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -78,9 +92,14 @@ class ImeInsetsSynchronizer private constructor(
|
||||
|
||||
// Workaround for https://issuetracker.google.com/issues/369223558
|
||||
// We expect the keyboard to have a bigger height than the OS navigation bar.
|
||||
if (keyboardHeight <= lastWindowInsets.navigationBarInsetHeight) {
|
||||
if (keyboardHeight <= getNavbarHeight()) {
|
||||
keyboardHeight = 0
|
||||
}
|
||||
|
||||
onIMEAnimationStarted(
|
||||
isKeyboardShowingUp,
|
||||
calculateBottomMargin(keyboardHeight, getNavbarHeight()),
|
||||
)
|
||||
}
|
||||
|
||||
return super.onStart(animation, bounds)
|
||||
@ -101,7 +120,7 @@ class ImeInsetsSynchronizer private constructor(
|
||||
targetView.updateBottomMargin(
|
||||
calculateBottomMargin(
|
||||
(keyboardHeight * imeAnimationFractionBasedOnDirection).toInt(),
|
||||
lastWindowInsets.navigationBarInsetHeight,
|
||||
getNavbarHeight(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -134,6 +153,9 @@ class ImeInsetsSynchronizer private constructor(
|
||||
false -> 0
|
||||
}
|
||||
|
||||
private fun getNavbarHeight() = ViewCompat.getRootWindowInsets(targetView)
|
||||
?.getInsets(systemBars())?.bottom ?: lastWindowInsets.navigationBarInsetHeight
|
||||
|
||||
private fun calculateBottomMargin(
|
||||
keyboardHeight: Int,
|
||||
navigationBarHeight: Int,
|
||||
@ -150,11 +172,17 @@ class ImeInsetsSynchronizer private constructor(
|
||||
* This works only on Android 10+, otherwise the dynamic padding based on the keyboard is not reliable.
|
||||
*
|
||||
* @param targetView The root view to add paddings to for accounting the visible keyboard height.
|
||||
* @param onIMEAnimationStarted Callback for when the IME animation starts.
|
||||
* It will inform whether the keyboard is showing or hiding and the height of the keyboard.
|
||||
* @param onIMEAnimationFinished Callback for when the IME animation finishes.
|
||||
* It will inform whether the keyboard is showing or hiding and the height of the keyboard.
|
||||
*/
|
||||
fun setup(
|
||||
targetView: View,
|
||||
onIMEAnimationStarted: (Boolean, Int) -> Unit = { _, _ -> },
|
||||
onIMEAnimationFinished: (Boolean, Int) -> Unit = { _, _ -> },
|
||||
) = when (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
true -> ImeInsetsSynchronizer(targetView)
|
||||
true -> ImeInsetsSynchronizer(targetView, onIMEAnimationStarted, onIMEAnimationFinished)
|
||||
false -> null
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class ImeInsetsSynchronizerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN ime animation is not in progress WHEN window insets change THEN update target view margins to be shown on top of the keyboard`() {
|
||||
fun `GIVEN ime animation in progress WHEN window insets change THEN don't update any margins`() {
|
||||
val windowInsets: WindowInsetsCompat = mock()
|
||||
doReturn(Insets.of(1, 2, 3, 4)).`when`(windowInsets).getInsets(ime())
|
||||
doReturn(Insets.of(10, 20, 30, 40)).`when`(windowInsets).getInsets(systemBars())
|
||||
@ -56,7 +56,7 @@ class ImeInsetsSynchronizerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN ime animation in progress WHEN window insets change THEN don't update any margins`() {
|
||||
fun `GIVEN ime animation is not in progress WHEN window insets change THEN update target view margins to be shown on top of the keyboard`() {
|
||||
val windowInsets: WindowInsetsCompat = mock()
|
||||
doReturn(Insets.of(1, 2, 3, 4)).`when`(windowInsets).getInsets(ime())
|
||||
doReturn(Insets.of(10, 20, 30, 40)).`when`(windowInsets).getInsets(systemBars())
|
||||
@ -68,6 +68,134 @@ class ImeInsetsSynchronizerTest {
|
||||
verify(targetView).requestLayout()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN ime animation is not in progress and it is hidden WHEN window insets change THEN inform about the current keyboard status`() {
|
||||
var isKeyboardShowing = "unknown"
|
||||
var keyboardAnimationFinishedHeight = -1
|
||||
val synchronizer = ImeInsetsSynchronizer.setup(
|
||||
view = targetView,
|
||||
onIMEAnimationStarted = { _, height ->
|
||||
isKeyboardShowing = "error"
|
||||
keyboardAnimationFinishedHeight = height + 22
|
||||
},
|
||||
onIMEAnimationFinished = { keyboardShowing, height ->
|
||||
isKeyboardShowing = keyboardShowing.toString()
|
||||
keyboardAnimationFinishedHeight = height
|
||||
},
|
||||
)!!
|
||||
val windowInsets: WindowInsetsCompat = mock()
|
||||
doReturn(Insets.of(0, 0, 0, 0)).`when`(windowInsets).getInsets(ime())
|
||||
doReturn(Insets.of(10, 20, 30, 40)).`when`(windowInsets).getInsets(systemBars())
|
||||
// Set that the keyboard is hidden
|
||||
doReturn(false).`when`(windowInsets).isVisible(ime())
|
||||
|
||||
val result = synchronizer.onApplyWindowInsets(targetView, windowInsets)
|
||||
|
||||
assertEquals(windowInsets, result)
|
||||
verify(targetViewLayoutParams).setMargins(0, 0, 0, 0)
|
||||
verify(targetView).requestLayout()
|
||||
assertEquals("false", isKeyboardShowing)
|
||||
assertEquals(0, keyboardAnimationFinishedHeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN ime animation is not in progress and it is shown WHEN window insets change THEN inform about the current keyboard status`() {
|
||||
var isKeyboardShowing = "unknown"
|
||||
var keyboardAnimationFinishedHeight = -1
|
||||
val synchronizer = ImeInsetsSynchronizer.setup(
|
||||
view = targetView,
|
||||
onIMEAnimationStarted = { _, height ->
|
||||
isKeyboardShowing = "error"
|
||||
keyboardAnimationFinishedHeight = height + 22
|
||||
},
|
||||
onIMEAnimationFinished = { keyboardShowing, height ->
|
||||
isKeyboardShowing = keyboardShowing.toString()
|
||||
keyboardAnimationFinishedHeight = height
|
||||
},
|
||||
)!!
|
||||
val windowInsets: WindowInsetsCompat = mock()
|
||||
doReturn(Insets.of(0, 0, 0, 1000)).`when`(windowInsets).getInsets(ime())
|
||||
doReturn(Insets.of(10, 20, 30, 40)).`when`(windowInsets).getInsets(systemBars())
|
||||
// Set that the keyboard is shown
|
||||
doReturn(true).`when`(windowInsets).isVisible(ime())
|
||||
|
||||
val result = synchronizer.onApplyWindowInsets(targetView, windowInsets)
|
||||
|
||||
assertEquals(windowInsets, result)
|
||||
verify(targetViewLayoutParams).setMargins(0, 0, 0, 960)
|
||||
verify(targetView).requestLayout()
|
||||
assertEquals("true", isKeyboardShowing)
|
||||
assertEquals(960, keyboardAnimationFinishedHeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the keyboard starts to hide THEN inform about the current keyboard status`() {
|
||||
var isKeyboardShowing = "unknown"
|
||||
var keyboardAnimationFinishedHeight = -1
|
||||
val synchronizer = ImeInsetsSynchronizer.setup(
|
||||
view = targetView,
|
||||
onIMEAnimationStarted = { keyboardShowing, height ->
|
||||
isKeyboardShowing = keyboardShowing.toString()
|
||||
keyboardAnimationFinishedHeight = height
|
||||
},
|
||||
onIMEAnimationFinished = { _, height ->
|
||||
isKeyboardShowing = "error"
|
||||
keyboardAnimationFinishedHeight = height + 22
|
||||
},
|
||||
)!!
|
||||
val windowInsets: WindowInsetsCompat = mock()
|
||||
doReturn(Insets.of(0, 0, 0, 1000)).`when`(windowInsets).getInsets(ime())
|
||||
doReturn(Insets.of(10, 20, 30, 40)).`when`(windowInsets).getInsets(systemBars())
|
||||
// Set that the keyboard is now considered hidden, just need to animate to that state
|
||||
doReturn(false).`when`(windowInsets).isVisible(ime())
|
||||
val imeAnimation: WindowInsetsAnimationCompat = mock()
|
||||
doReturn(ime()).`when`(imeAnimation).typeMask
|
||||
val animationBounds: WindowInsetsAnimationCompat.BoundsCompat = mock()
|
||||
doReturn(Insets.of(0, 0, 0, 200)).`when`(animationBounds).lowerBound
|
||||
doReturn(Insets.of(0, 0, 0, 1200)).`when`(animationBounds).upperBound
|
||||
// Ensure the current window insets are known
|
||||
synchronizer.onApplyWindowInsets(targetView, windowInsets)
|
||||
|
||||
synchronizer.onStart(imeAnimation, animationBounds)
|
||||
|
||||
assertEquals("false", isKeyboardShowing)
|
||||
assertEquals(1000, keyboardAnimationFinishedHeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the keyboard starts to show THEN inform about the current keyboard status`() {
|
||||
var isKeyboardShowing = "unknown"
|
||||
var keyboardAnimationFinishedHeight = -1
|
||||
val synchronizer = ImeInsetsSynchronizer.setup(
|
||||
view = targetView,
|
||||
onIMEAnimationStarted = { keyboardShowing, height ->
|
||||
isKeyboardShowing = keyboardShowing.toString()
|
||||
keyboardAnimationFinishedHeight = height
|
||||
},
|
||||
onIMEAnimationFinished = { _, height ->
|
||||
isKeyboardShowing = "error"
|
||||
keyboardAnimationFinishedHeight = height + 22
|
||||
},
|
||||
)!!
|
||||
val windowInsets: WindowInsetsCompat = mock()
|
||||
doReturn(Insets.of(0, 0, 0, 1000)).`when`(windowInsets).getInsets(ime())
|
||||
doReturn(Insets.of(10, 20, 30, 40)).`when`(windowInsets).getInsets(systemBars())
|
||||
// Set that the keyboard starts to show
|
||||
doReturn(true).`when`(windowInsets).isVisible(ime())
|
||||
val imeAnimation: WindowInsetsAnimationCompat = mock()
|
||||
doReturn(ime()).`when`(imeAnimation).typeMask
|
||||
val animationBounds: WindowInsetsAnimationCompat.BoundsCompat = mock()
|
||||
doReturn(Insets.of(0, 0, 0, 200)).`when`(animationBounds).lowerBound
|
||||
doReturn(Insets.of(0, 0, 0, 1200)).`when`(animationBounds).upperBound
|
||||
// Ensure the current window insets are known
|
||||
synchronizer.onApplyWindowInsets(targetView, windowInsets)
|
||||
|
||||
synchronizer.onStart(imeAnimation, animationBounds)
|
||||
|
||||
assertEquals("true", isKeyboardShowing)
|
||||
assertEquals(960, keyboardAnimationFinishedHeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a show keyboard animation WHEN the progress is updated THEN update the targetView to be shown above the keyboard`() {
|
||||
// Set the initial system insets
|
||||
|
@ -130,6 +130,7 @@ import mozilla.components.support.base.feature.ActivityResultHandler
|
||||
import mozilla.components.support.base.feature.PermissionsFeature
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import mozilla.components.support.ktx.android.view.ImeInsetsSynchronizer
|
||||
import mozilla.components.support.ktx.android.view.enterImmersiveMode
|
||||
import mozilla.components.support.ktx.android.view.exitImmersiveMode
|
||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
||||
@ -457,6 +458,7 @@ abstract class BaseBrowserFragment :
|
||||
val tab = getCurrentTab()
|
||||
browserInitialized = if (tab != null) {
|
||||
initializeUI(view, tab)
|
||||
setupIMEInsetsHandling(view)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@ -2453,6 +2455,8 @@ abstract class BaseBrowserFragment :
|
||||
reinitializeMicrosurveyPrompt = ::initializeMicrosurveyPrompt,
|
||||
)
|
||||
}
|
||||
|
||||
view?.let { setupIMEInsetsHandling(it) }
|
||||
}
|
||||
|
||||
private fun reinitializeNavBar() {
|
||||
@ -2654,4 +2658,36 @@ abstract class BaseBrowserFragment :
|
||||
navController.navigate(directions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupIMEInsetsHandling(view: View) {
|
||||
if (context?.settings()?.toolbarPosition != ToolbarPosition.BOTTOM) return
|
||||
|
||||
val toolbar = listOf(
|
||||
_bottomToolbarContainerView?.toolbarContainerView,
|
||||
_browserToolbarView?.layout,
|
||||
).firstOrNull { it != null } ?: return
|
||||
|
||||
ImeInsetsSynchronizer.setup(
|
||||
targetView = toolbar,
|
||||
onIMEAnimationStarted = { isKeyboardShowingUp, keyboardHeight ->
|
||||
// If the keyboard is hiding have the engine view immediately expand to the entire height of the
|
||||
// screen and ensure the toolbar is shown above keyboard before both would be animated down.
|
||||
if (!isKeyboardShowingUp) {
|
||||
(view.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin = 0
|
||||
(toolbar.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin = keyboardHeight
|
||||
view.requestLayout()
|
||||
}
|
||||
},
|
||||
onIMEAnimationFinished = { isKeyboardShowingUp, keyboardHeight ->
|
||||
// If the keyboard is showing up keep the engine view covering the entire height
|
||||
// of the screen until the animation is finished to avoid reflowing the web content
|
||||
// together with the keyboard animation in a short burst of updates.
|
||||
if (isKeyboardShowingUp) {
|
||||
(view.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin = keyboardHeight
|
||||
(toolbar.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin = 0
|
||||
view.requestLayout()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user