commit 41132b91b93cd40956038a41a2ba77054a98a3e0
parent 77a9fc3c9007ed836305d378deefa16037922e61
Author: Mugurell <Mugurell@users.noreply.github.com>
Date: Thu, 2 Oct 2025 11:01:19 +0000
Bug 1991459 - Add new insetsSource parameter to ImeInsetsSynchronizer r=android-reviewers,skhan
We recently observed that using a ComposeView as listener for insets
updates does not always work.
Using a normal View as an insets listener gives us the needed updates
but using these to smoothly animate View's bounds might results in UI
artifacts if the View cannot reflow content in time.
So this patch comes to allow using a View as insets listeners and allows
for a smaller View or ComposeView as the target View that should be
animated in response to the keyboard animating on the screen.
Differential Revision: https://phabricator.services.mozilla.com/D266766
Diffstat:
2 files changed, 74 insertions(+), 31 deletions(-)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/ImeInsetsSynchronizer.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/ImeInsetsSynchronizer.kt
@@ -23,6 +23,8 @@ 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 insetsSource The view providing insets data. Using [ComposeView] is not recommended
+ * as this is not guaranteed to provide the correct insets data.
* @param synchronizeViewWithIME Whether to automatically apply the needed margins to [targetView]
* to ensure it will be animated together with the keyboard or not. As an alternative integrators can use
* the [onIMEAnimationStarted] and [onIMEAnimationFinished] callbacks to resize the layout on their own.
@@ -33,6 +35,7 @@ import androidx.core.view.WindowInsetsCompat.Type.systemBars
*/
class ImeInsetsSynchronizer private constructor(
private val targetView: View,
+ private val insetsSource: View,
private val synchronizeViewWithIME: Boolean,
private val onIMEAnimationStarted: (Boolean, Int) -> Unit,
private val onIMEAnimationFinished: (Boolean, Int) -> Unit,
@@ -40,8 +43,8 @@ class ImeInsetsSynchronizer private constructor(
OnApplyWindowInsetsListener {
init {
- ViewCompat.setWindowInsetsAnimationCallback(targetView, this)
- ViewCompat.setOnApplyWindowInsetsListener(targetView, this)
+ ViewCompat.setWindowInsetsAnimationCallback(insetsSource, this)
+ ViewCompat.setOnApplyWindowInsetsListener(insetsSource, this)
}
private lateinit var lastWindowInsets: WindowInsetsCompat
@@ -58,7 +61,7 @@ class ImeInsetsSynchronizer private constructor(
isKeyboardShowingUp = windowInsets.isKeyboardShowingUp
if (!areKeyboardInsetsDeferred) {
- view.updateBottomMargin(
+ updateTargetBottomMargin(
calculateBottomMargin(
windowInsets.keyboardInsets.bottom,
getNavbarHeight(),
@@ -126,7 +129,7 @@ class ImeInsetsSynchronizer private constructor(
false -> 1 - imeAnimation.interpolatedFraction
}
- targetView.updateBottomMargin(
+ updateTargetBottomMargin(
calculateBottomMargin(
(keyboardHeight * imeAnimationFractionBasedOnDirection).toInt(),
getNavbarHeight(),
@@ -165,12 +168,12 @@ class ImeInsetsSynchronizer private constructor(
false -> 0
}
- private fun getNavbarHeight() = ViewCompat.getRootWindowInsets(targetView)
+ private fun getNavbarHeight() = ViewCompat.getRootWindowInsets(insetsSource)
?.getInsets(systemBars())?.bottom ?: lastWindowInsets.navigationBarInsetHeight
private fun getCurrentInsets() = when (::lastWindowInsets.isInitialized) {
true -> lastWindowInsets
- false -> ViewCompat.getRootWindowInsets(targetView)
+ false -> ViewCompat.getRootWindowInsets(insetsSource)
}
private fun calculateBottomMargin(
@@ -178,10 +181,12 @@ class ImeInsetsSynchronizer private constructor(
navigationBarHeight: Int,
) = (keyboardHeight - navigationBarHeight).coerceAtLeast(0)
- private fun View.updateBottomMargin(bottom: Int) {
+ private fun updateTargetBottomMargin(bottom: Int) {
if (synchronizeViewWithIME) {
- (layoutParams as ViewGroup.MarginLayoutParams).setMargins(0, 0, 0, bottom)
- requestLayout()
+ with(targetView) {
+ (layoutParams as ViewGroup.MarginLayoutParams).setMargins(0, 0, 0, bottom)
+ requestLayout()
+ }
}
}
@@ -190,7 +195,9 @@ class ImeInsetsSynchronizer private constructor(
* Setup animating [targetView] as always on top of the keyboard while also respecting all system bars insets.
* This works only on Android 13+, 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 targetView The view to add paddings to for accounting the visible keyboard height.
+ * @param insetsSource The view providing insets data. Using [ComposeView] is not recommended
+ * as this is not guaranteed to provide the correct insets data.
* @param synchronizeViewWithIME Whether to automatically apply the needed margins to [targetView]
* to ensure it will be animated together with the keyboard or not. As an alternative integrators can use
* the [onIMEAnimationStarted] and [onIMEAnimationFinished] callbacks to resize the layout on their own.
@@ -201,12 +208,14 @@ class ImeInsetsSynchronizer private constructor(
*/
fun setup(
targetView: View,
+ insetsSource: View = targetView,
synchronizeViewWithIME: Boolean = true,
onIMEAnimationStarted: (Boolean, Int) -> Unit = { _, _ -> },
onIMEAnimationFinished: (Boolean, Int) -> Unit = { _, _ -> },
) = when (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
true -> ImeInsetsSynchronizer(
targetView,
+ insetsSource,
synchronizeViewWithIME,
onIMEAnimationStarted,
onIMEAnimationFinished,
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
@@ -2548,27 +2548,61 @@ abstract class BaseBrowserFragment :
}
private fun setupIMEInsetsHandling(view: View) {
- ImeInsetsSynchronizer.setup(
- targetView = view,
- synchronizeViewWithIME = context?.settings()?.toolbarPosition == ToolbarPosition.BOTTOM,
- 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
- 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 || keyboardHeight == 0) {
- (view.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin = keyboardHeight
- view.requestLayout()
- }
- },
- )
+ when (context?.settings()?.toolbarPosition) {
+ ToolbarPosition.BOTTOM -> {
+ val toolbar = listOf(
+ _bottomToolbarContainerView?.toolbarContainerView,
+ _browserToolbarView?.layout,
+ ).firstOrNull { it != null } ?: return
+
+ ImeInsetsSynchronizer.setup(
+ targetView = toolbar,
+ insetsSource = view,
+ 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 || keyboardHeight == 0) {
+ (view.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin = keyboardHeight
+ (toolbar.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin = 0
+ view.requestLayout()
+ }
+ },
+ )
+ }
+
+ ToolbarPosition.TOP -> {
+ ImeInsetsSynchronizer.setup(
+ targetView = view,
+ synchronizeViewWithIME = false,
+ onIMEAnimationStarted = { isKeyboardShowingUp, _ ->
+ if (!isKeyboardShowingUp) {
+ (view.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin = 0
+ view.requestLayout()
+ }
+ },
+ onIMEAnimationFinished = { isKeyboardShowingUp, keyboardHeight ->
+ if (isKeyboardShowingUp || keyboardHeight == 0) {
+ (view.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin = keyboardHeight
+ view.requestLayout()
+ }
+ },
+ )
+ }
+
+ else -> {
+ // no-op
+ }
+ }
}
private fun openManageStorageSettings() {