Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.VisibleForTesting
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.lifecycle.Lifecycle.State.STARTED
Expand Down Expand Up @@ -64,6 +65,7 @@ import com.duckduckgo.app.browser.shortcut.ShortcutBuilder
import com.duckduckgo.app.browser.tabs.TabManager
import com.duckduckgo.app.browser.tabs.TabManager.TabModel
import com.duckduckgo.app.browser.tabs.adapter.TabPagerAdapter
import com.duckduckgo.app.browser.ui.InsetsWithKeyboardCallback
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.dispatchers.ExternalIntentProcessingState
Expand Down Expand Up @@ -368,6 +370,10 @@ open class BrowserActivity : DuckDuckGoActivity() {
}
configureOnBackPressedListener()
showNewAddressBarOptionChoiceScreen()

val insetsWithKeyboardCallback = InsetsWithKeyboardCallback(window)
ViewCompat.setOnApplyWindowInsetsListener(binding.root, insetsWithKeyboardCallback)
ViewCompat.setWindowInsetsAnimationCallback(binding.root, insetsWithKeyboardCallback)
}

override fun onSaveInstanceState(outState: Bundle) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ import com.duckduckgo.app.browser.omnibar.Omnibar.OmnibarTextState
import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode
import com.duckduckgo.app.browser.omnibar.OmnibarItemPressedListener
import com.duckduckgo.app.browser.omnibar.QueryOrigin
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition.BOTTOM
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition.TOP
import com.duckduckgo.app.browser.print.PrintDocumentAdapterFactory
Expand All @@ -157,6 +158,7 @@ import com.duckduckgo.app.browser.session.WebViewSessionStorage
import com.duckduckgo.app.browser.shortcut.ShortcutBuilder
import com.duckduckgo.app.browser.tabpreview.WebViewPreviewGenerator
import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister
import com.duckduckgo.app.browser.ui.InsetsWithKeyboardAnimationCallback
import com.duckduckgo.app.browser.ui.dialogs.AutomaticFireproofDialogOptions
import com.duckduckgo.app.browser.ui.dialogs.LaunchInExternalAppOptions
import com.duckduckgo.app.browser.ui.dialogs.widgetprompt.AlternativeHomeScreenWidgetBottomSheetDialog
Expand Down Expand Up @@ -1044,6 +1046,11 @@ class BrowserTabFragment :
}

launchDownloadMessagesJob()

if (omnibar.omnibarPosition == OmnibarPosition.BOTTOM) {
val insetsWithKeyboardAnimationCallback = InsetsWithKeyboardAnimationCallback(omnibar.newOmnibar)
ViewCompat.setWindowInsetsAnimationCallback(omnibar.newOmnibar, insetsWithKeyboardAnimationCallback)
}
}

private fun updateOrDeleteWebViewPreview() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.browser.ui

import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat

// Credit: https://github.com/johncodeos-blog/MoveViewWithKeyboardAndroidExample
class InsetsWithKeyboardAnimationCallback(private val view: View) : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())

val diff = Insets.subtract(imeInsets, systemInsets).let {
Insets.max(it, Insets.NONE)
}

view.translationX = (diff.left - diff.right).toFloat()
view.translationY = (diff.top - diff.bottom).toFloat()

return insets
}

override fun onEnd(animation: WindowInsetsAnimationCompat) {
view.translationX = 0f
view.translationY = 0f
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.browser.ui

import android.os.Build
import android.view.View
import android.view.Window
import android.view.WindowManager
import androidx.core.view.*

// Credit: https://github.com/johncodeos-blog/MoveViewWithKeyboardAndroidExample
class InsetsWithKeyboardCallback(window: Window) : OnApplyWindowInsetsListener, WindowInsetsAnimationCompat.Callback(
DISPATCH_MODE_CONTINUE_ON_SUBTREE,
) {
private var deferredInsets = false
private var view: View? = null
private var lastWindowInsets: WindowInsetsCompat? = null

init {
WindowCompat.setDecorFitsSystemWindows(window, false)

// For better support for devices API 29 and lower
if (Build.VERSION.SDK_INT <= 29) {
@Suppress("DEPRECATION")
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
}
}

override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
view = v
lastWindowInsets = insets
val types = when {
// When the deferred flag is enabled, we only use the systemBars() insets
deferredInsets -> WindowInsetsCompat.Type.systemBars()
// When the deferred flag is disabled, we use combination of the the systemBars() and ime() insets
else -> WindowInsetsCompat.Type.systemBars() + WindowInsetsCompat.Type.ime()
}

val typeInsets = insets.getInsets(types)
v.setPadding(typeInsets.left, typeInsets.top, typeInsets.right, typeInsets.bottom)
return WindowInsetsCompat.CONSUMED
}

override fun onPrepare(animation: WindowInsetsAnimationCompat) {
if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
// When the IME is not visible, we defer the WindowInsetsCompat.Type.ime() insets
deferredInsets = true
}
}

override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
return insets
}

override fun onEnd(animation: WindowInsetsAnimationCompat) {
if (deferredInsets && (animation.typeMask and WindowInsetsCompat.Type.ime()) != 0) {
// When the IME animation has finished and the IME inset has been deferred, we reset the flag
deferredInsets = false

// We dispatch insets manually because if we let the normal dispatch cycle handle it, this will happen too late and cause a visual flicker
// So we dispatch the latest WindowInsets to the view
if (lastWindowInsets != null && view != null) {
ViewCompat.dispatchApplyWindowInsets(view!!, lastWindowInsets!!)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.duckduckgo.app.browser.omnibar.OmnibarLayout
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
import kotlin.math.roundToInt

/**
* A [ScrollingViewBehavior] that observes [AppBarLayout] (top omnibar) present in the view hierarchy and applies top offset to the child view
Expand Down Expand Up @@ -108,7 +109,9 @@ private fun offsetByBottomElementVisibleHeight(
val newBottomPadding = if (dependency.isGone) {
0
} else {
dependency.measuredHeight - dependency.translationY.toInt()
// Clamp negative translation (e.g., IME pushing the bar up) to avoid inflating padding
val clampedTranslationY = if (dependency.translationY > 0f) dependency.translationY.roundToInt() else 0
(dependency.measuredHeight - clampedTranslationY).coerceAtLeast(0)
}
return if (child.paddingBottom != newBottomPadding) {
child.setPadding(
Expand Down
Loading