diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 0e56be079caf..24605a09f5a2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -246,7 +246,6 @@ import com.duckduckgo.browser.api.ui.BrowserScreens.PrivateSearchScreenNoParams import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams import com.duckduckgo.common.ui.DuckDuckGoActivity import com.duckduckgo.common.ui.DuckDuckGoFragment -import com.duckduckgo.common.ui.anim.AnimationResourceProvider import com.duckduckgo.common.ui.experiments.visual.store.ExperimentalThemingDataStore import com.duckduckgo.common.ui.store.BrowserAppTheme import com.duckduckgo.common.ui.tabs.SwipingTabsFeatureProvider @@ -288,6 +287,7 @@ import com.duckduckgo.downloads.api.DownloadsFileActions import com.duckduckgo.downloads.api.FileDownloader import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload import com.duckduckgo.duckchat.api.DuckChat +import com.duckduckgo.duckchat.api.inputscreen.BrowserAndInputScreenTransitionProvider import com.duckduckgo.duckchat.impl.inputscreen.ui.InputScreenActivity.Companion.QUERY import com.duckduckgo.duckchat.impl.inputscreen.ui.InputScreenActivity.Companion.TAB_ID import com.duckduckgo.duckchat.impl.inputscreen.ui.InputScreenActivityParams @@ -574,6 +574,9 @@ class BrowserTabFragment : @Inject lateinit var omnibarTypeResolver: OmnibarTypeResolver + @Inject + lateinit var browserAndInputScreenTransitionProvider: BrowserAndInputScreenTransitionProvider + /** * We use this to monitor whether the user was seeing the in-context Email Protection signup prompt * This is needed because the activity stack will be cleared if an external link is opened in our browser @@ -1075,8 +1078,8 @@ class BrowserTabFragment : requireContext(), InputScreenActivityParams(query = query), ) - val enterTransition = AnimationResourceProvider.getSlideInFromTopFadeIn() - val exitTransition = AnimationResourceProvider.getSlideOutToBottomFadeOut() + val enterTransition = browserAndInputScreenTransitionProvider.getInputScreenEnterAnimation() + val exitTransition = browserAndInputScreenTransitionProvider.getBrowserExitAnimation() val options = ActivityOptionsCompat.makeCustomAnimation( requireActivity(), enterTransition, diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/anim/AnimationResourceProvider.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/anim/AnimationResourceProvider.kt deleted file mode 100644 index 2bb38527b83b..000000000000 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/anim/AnimationResourceProvider.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.common.ui.anim - -import android.os.Build.VERSION -import com.duckduckgo.mobile.android.R - -object AnimationResourceProvider { - - // The overshoot alpha transition can result in a black screen flash on older Android devices, - // so we use the standard decelerate transition instead for these cases in the provider functions below. - // The check is for version 34, as that's the range of devices on which we tested and confirmed that it works. - - fun getSlideInFromBottomFadeIn(): Int { - return if (VERSION.SDK_INT >= 34) { - R.anim.slide_in_from_bottom_fade_in_overshoot - } else { - R.anim.slide_in_from_bottom_fade_in - } - } - - fun getSlideOutToTopFadeOut(): Int { - return if (VERSION.SDK_INT >= 34) { - R.anim.slide_out_to_top_fade_out_overshoot - } else { - R.anim.slide_out_to_top_fade_out - } - } - - fun getSlideInFromTopFadeIn(): Int { - return if (VERSION.SDK_INT >= 34) { - R.anim.slide_in_from_top_fade_in_overshoot - } else { - R.anim.slide_in_from_top_fade_in - } - } - - fun getSlideOutToBottomFadeOut(): Int { - return if (VERSION.SDK_INT >= 34) { - R.anim.slide_out_to_bottom_fade_out_overshoot - } else { - R.anim.slide_out_to_bottom_fade_out - } - } -} diff --git a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/inputscreen/BrowserAndInputScreenTransitionProvider.kt b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/inputscreen/BrowserAndInputScreenTransitionProvider.kt new file mode 100644 index 000000000000..c691d7f234bd --- /dev/null +++ b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/inputscreen/BrowserAndInputScreenTransitionProvider.kt @@ -0,0 +1,31 @@ +/* + * 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.duckchat.api.inputscreen + +/** + * Provides animation resources for activity transitions between browser and input screen. + */ +interface BrowserAndInputScreenTransitionProvider { + + fun getBrowserEnterAnimation(): Int + + fun getBrowserExitAnimation(): Int + + fun getInputScreenEnterAnimation(): Int + + fun getInputScreenExitAnimation(): Int +} diff --git a/common/common-ui/src/main/res/anim/slide_in_from_bottom_fade_in_overshoot.xml b/duckchat/duckchat-api/src/main/res/anim-v33/slide_in_from_bottom_fade_in_dark.xml similarity index 71% rename from common/common-ui/src/main/res/anim/slide_in_from_bottom_fade_in_overshoot.xml rename to duckchat/duckchat-api/src/main/res/anim-v33/slide_in_from_bottom_fade_in_dark.xml index 35a9fc0ee607..2b839ad3edc9 100644 --- a/common/common-ui/src/main/res/anim/slide_in_from_bottom_fade_in_overshoot.xml +++ b/duckchat/duckchat-api/src/main/res/anim-v33/slide_in_from_bottom_fade_in_dark.xml @@ -1,5 +1,4 @@ - - + android:backdropColor="@color/background_background_dark" + android:duration="@integer/slide_animation_duration_ms" + android:showBackdrop="true"> \ No newline at end of file diff --git a/common/common-ui/src/main/res/anim/slide_in_from_top_fade_in_overshoot.xml b/duckchat/duckchat-api/src/main/res/anim-v33/slide_in_from_bottom_fade_in_light.xml similarity index 68% rename from common/common-ui/src/main/res/anim/slide_in_from_top_fade_in_overshoot.xml rename to duckchat/duckchat-api/src/main/res/anim-v33/slide_in_from_bottom_fade_in_light.xml index e0a7e572665f..d6a8fde9e84e 100644 --- a/common/common-ui/src/main/res/anim/slide_in_from_top_fade_in_overshoot.xml +++ b/duckchat/duckchat-api/src/main/res/anim-v33/slide_in_from_bottom_fade_in_light.xml @@ -1,5 +1,4 @@ - - + android:backdropColor="@color/background_background_light" + android:duration="@integer/slide_animation_duration_ms" + android:showBackdrop="true"> \ No newline at end of file diff --git a/common/common-ui/src/main/res/anim/slide_in_from_top_fade_in.xml b/duckchat/duckchat-api/src/main/res/anim-v33/slide_in_from_top_fade_in.xml similarity index 77% rename from common/common-ui/src/main/res/anim/slide_in_from_top_fade_in.xml rename to duckchat/duckchat-api/src/main/res/anim-v33/slide_in_from_top_fade_in.xml index 3a8753d8433b..f1ab7ef18e76 100644 --- a/common/common-ui/src/main/res/anim/slide_in_from_top_fade_in.xml +++ b/duckchat/duckchat-api/src/main/res/anim-v33/slide_in_from_top_fade_in.xml @@ -1,5 +1,4 @@ - - + android:duration="@integer/slide_animation_duration_ms"> \ No newline at end of file diff --git a/common/common-ui/src/main/res/anim/slide_out_to_bottom_fade_out_overshoot.xml b/duckchat/duckchat-api/src/main/res/anim-v33/slide_out_to_bottom_fade_out_dark.xml similarity index 71% rename from common/common-ui/src/main/res/anim/slide_out_to_bottom_fade_out_overshoot.xml rename to duckchat/duckchat-api/src/main/res/anim-v33/slide_out_to_bottom_fade_out_dark.xml index bb39e23bc50d..b9f4a81e70d9 100644 --- a/common/common-ui/src/main/res/anim/slide_out_to_bottom_fade_out_overshoot.xml +++ b/duckchat/duckchat-api/src/main/res/anim-v33/slide_out_to_bottom_fade_out_dark.xml @@ -1,5 +1,4 @@ - - + android:backdropColor="@color/background_background_dark" + android:duration="@integer/slide_animation_duration_ms" + android:showBackdrop="true"> \ No newline at end of file diff --git a/common/common-ui/src/main/res/anim/slide_out_to_top_fade_out_overshoot.xml b/duckchat/duckchat-api/src/main/res/anim-v33/slide_out_to_bottom_fade_out_light.xml similarity index 68% rename from common/common-ui/src/main/res/anim/slide_out_to_top_fade_out_overshoot.xml rename to duckchat/duckchat-api/src/main/res/anim-v33/slide_out_to_bottom_fade_out_light.xml index c13097b1311c..f9850f491ade 100644 --- a/common/common-ui/src/main/res/anim/slide_out_to_top_fade_out_overshoot.xml +++ b/duckchat/duckchat-api/src/main/res/anim-v33/slide_out_to_bottom_fade_out_light.xml @@ -1,5 +1,4 @@ - - + android:backdropColor="@color/background_background_light" + android:duration="@integer/slide_animation_duration_ms" + android:showBackdrop="true"> + android:interpolator="@anim/overshoot_interpolator_tension_1" + android:toYDelta="56dp" /> \ No newline at end of file diff --git a/common/common-ui/src/main/res/anim/slide_out_to_top_fade_out.xml b/duckchat/duckchat-api/src/main/res/anim-v33/slide_out_to_top_fade_out.xml similarity index 77% rename from common/common-ui/src/main/res/anim/slide_out_to_top_fade_out.xml rename to duckchat/duckchat-api/src/main/res/anim-v33/slide_out_to_top_fade_out.xml index 476006e91d91..a15fd26a46dc 100644 --- a/common/common-ui/src/main/res/anim/slide_out_to_top_fade_out.xml +++ b/duckchat/duckchat-api/src/main/res/anim-v33/slide_out_to_top_fade_out.xml @@ -1,5 +1,4 @@ - - + android:duration="@integer/slide_animation_duration_ms"> \ No newline at end of file diff --git a/common/common-ui/src/main/res/anim/slide_in_from_bottom_fade_in.xml b/duckchat/duckchat-api/src/main/res/anim/fade_in.xml similarity index 77% rename from common/common-ui/src/main/res/anim/slide_in_from_bottom_fade_in.xml rename to duckchat/duckchat-api/src/main/res/anim/fade_in.xml index 42e5984a76cb..90b8fdf2c24e 100644 --- a/common/common-ui/src/main/res/anim/slide_in_from_bottom_fade_in.xml +++ b/duckchat/duckchat-api/src/main/res/anim/fade_in.xml @@ -1,5 +1,4 @@ - - - - + android:duration="@integer/slide_animation_duration_ms"> \ No newline at end of file diff --git a/common/common-ui/src/main/res/anim/slide_out_to_bottom_fade_out.xml b/duckchat/duckchat-api/src/main/res/anim/fade_out.xml similarity index 77% rename from common/common-ui/src/main/res/anim/slide_out_to_bottom_fade_out.xml rename to duckchat/duckchat-api/src/main/res/anim/fade_out.xml index 49d223c84b89..82629cdc47b7 100644 --- a/common/common-ui/src/main/res/anim/slide_out_to_bottom_fade_out.xml +++ b/duckchat/duckchat-api/src/main/res/anim/fade_out.xml @@ -1,5 +1,4 @@ - - - - + android:duration="@integer/slide_animation_duration_ms"> \ No newline at end of file diff --git a/common/common-ui/src/main/res/anim/overshoot_interpolator_tension_1.xml b/duckchat/duckchat-api/src/main/res/anim/overshoot_interpolator_tension_1.xml similarity index 88% rename from common/common-ui/src/main/res/anim/overshoot_interpolator_tension_1.xml rename to duckchat/duckchat-api/src/main/res/anim/overshoot_interpolator_tension_1.xml index 6f8515bc8193..211b9ba48077 100644 --- a/common/common-ui/src/main/res/anim/overshoot_interpolator_tension_1.xml +++ b/duckchat/duckchat-api/src/main/res/anim/overshoot_interpolator_tension_1.xml @@ -14,6 +14,5 @@ ~ limitations under the License. --> - \ No newline at end of file diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/BrowserAndInputScreenTransitionProviderImpl.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/BrowserAndInputScreenTransitionProviderImpl.kt new file mode 100644 index 000000000000..255ff1bf0206 --- /dev/null +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/BrowserAndInputScreenTransitionProviderImpl.kt @@ -0,0 +1,91 @@ +/* + * 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.duckchat.impl.inputscreen.ui + +import android.os.Build.VERSION +import com.duckduckgo.common.ui.store.AppTheme +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.duckchat.api.R +import com.duckduckgo.duckchat.api.inputscreen.BrowserAndInputScreenTransitionProvider +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject + +/** + * Provides animation resources for activity transitions between browser and input screen. + * + * ### API Level Behavior + * - **API < 33**: Uses simple fade animations as slide animations don't seem to be supported (on tested devices). + * - **API ≥ 33**: Uses slide animations with fade effects. + * + * ### Backdrop Color Handling + * Alpha transitions on activities fade the entire window, revealing the black system background. + * To prevent this visual artifact (especially noticeable in light mode), browser animations use + * a backdrop color that matches the activity background. + * Only one animation component needs backdrop color to prevent system background from showing through, + * so we're only applying it to browser animations. + * + * **Limitations:** + * - Backdrop color API is only available from API 33+, so black still shows through on lower APIs, + * but it's less dramatic because there's no slide animation. + * - Cannot use themeable attributes (`?attr`) as they cause crashes of the whole launcher (on tested devices). + * Instead, we use fixed color values and filter resources by current theme state. + */ +@ContributesBinding(scope = AppScope::class) +class BrowserAndInputScreenTransitionProviderImpl @Inject constructor( + private val appTheme: AppTheme, +) : BrowserAndInputScreenTransitionProvider { + + override fun getBrowserEnterAnimation(): Int { + return if (VERSION.SDK_INT >= 33) { + if (appTheme.isLightModeEnabled()) { + R.anim.slide_in_from_bottom_fade_in_light + } else { + R.anim.slide_in_from_bottom_fade_in_dark + } + } else { + R.anim.fade_in + } + } + + override fun getBrowserExitAnimation(): Int { + return if (VERSION.SDK_INT >= 33) { + if (appTheme.isLightModeEnabled()) { + R.anim.slide_out_to_bottom_fade_out_light + } else { + R.anim.slide_out_to_bottom_fade_out_dark + } + } else { + R.anim.fade_out + } + } + + override fun getInputScreenEnterAnimation(): Int { + return if (VERSION.SDK_INT >= 33) { + R.anim.slide_in_from_top_fade_in + } else { + R.anim.fade_in + } + } + + override fun getInputScreenExitAnimation(): Int { + return if (VERSION.SDK_INT >= 33) { + R.anim.slide_out_to_top_fade_out + } else { + R.anim.fade_out + } + } +} diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/InputScreenActivity.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/InputScreenActivity.kt index 9898b001d4f8..fc75e97c2381 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/InputScreenActivity.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/InputScreenActivity.kt @@ -21,10 +21,11 @@ import android.os.Bundle import com.duckduckgo.anvil.annotations.ContributeToActivityStarter import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.common.ui.DuckDuckGoActivity -import com.duckduckgo.common.ui.anim.AnimationResourceProvider import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.duckchat.api.inputscreen.BrowserAndInputScreenTransitionProvider import com.duckduckgo.duckchat.impl.R import com.duckduckgo.navigation.api.GlobalActivityStarter +import javax.inject.Inject data class InputScreenActivityParams( val query: String, @@ -34,6 +35,9 @@ data class InputScreenActivityParams( @ContributeToActivityStarter(InputScreenActivityParams::class) class InputScreenActivity : DuckDuckGoActivity() { + @Inject + lateinit var browserAndInputScreenTransitionProvider: BrowserAndInputScreenTransitionProvider + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_input_screen) @@ -45,8 +49,9 @@ class InputScreenActivity : DuckDuckGoActivity() { } private fun applyExitTransition() { - val enterTransition = AnimationResourceProvider.getSlideInFromBottomFadeIn() - val exitTransition = AnimationResourceProvider.getSlideOutToTopFadeOut() + val enterTransition = browserAndInputScreenTransitionProvider.getBrowserEnterAnimation() + val exitTransition = browserAndInputScreenTransitionProvider.getInputScreenExitAnimation() + if (VERSION.SDK_INT >= 34) { overrideActivityTransition( OVERRIDE_TRANSITION_CLOSE,