Skip to content

Commit 6746b3d

Browse files
authored
improve Duck.ai fragment transitions (#6331)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671518894266/task/1210696192460180?focus=true ### Description Improves transitions in and out of the Duck.ai screen. ### Steps to test this PR - [x] Stress test opening/closing of the Duck.ai screen and verify that the animations always play smoothly and end up in the right state. - [x] Disable tab swiping with https://www.jsonblob.com/api/1389979353823240192 and try again.
1 parent 025acd1 commit 6746b3d

File tree

2 files changed

+111
-15
lines changed

2 files changed

+111
-15
lines changed

app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ import androidx.webkit.ServiceWorkerControllerCompat
4545
import androidx.webkit.WebViewFeature
4646
import com.duckduckgo.anvil.annotations.InjectWith
4747
import com.duckduckgo.app.browser.BrowserViewModel.Command
48+
import com.duckduckgo.app.browser.animations.slideAndFadeInFromLeft
49+
import com.duckduckgo.app.browser.animations.slideAndFadeInFromRight
50+
import com.duckduckgo.app.browser.animations.slideAndFadeOutToLeft
51+
import com.duckduckgo.app.browser.animations.slideAndFadeOutToRight
4852
import com.duckduckgo.app.browser.databinding.ActivityBrowserBinding
4953
import com.duckduckgo.app.browser.databinding.IncludeExperimentalOmnibarToolbarMockupBinding
5054
import com.duckduckgo.app.browser.databinding.IncludeExperimentalOmnibarToolbarMockupBottomBinding
@@ -777,13 +781,11 @@ open class BrowserActivity : DuckDuckGoActivity() {
777781
isDuckChatVisible = false
778782
val fragment = duckAiFragment
779783
if (fragment?.isVisible == true) {
780-
val transaction = supportFragmentManager.beginTransaction()
781-
transaction.setCustomAnimations(
782-
com.duckduckgo.mobile.android.R.anim.slide_from_right,
783-
com.duckduckgo.mobile.android.R.anim.slide_to_right,
784-
)
785-
transaction.hide(fragment)
786-
transaction.commit()
784+
animateDuckAiFragmentOut {
785+
val transaction = supportFragmentManager.beginTransaction()
786+
transaction.hide(fragment)
787+
transaction.commit()
788+
}
787789
}
788790
}
789791

@@ -803,6 +805,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
803805
}
804806

805807
private fun launchNewDuckChat(duckChatUrl: String?) {
808+
val wasFragmentVisible = duckAiFragment?.isVisible ?: false
806809
val fragment = DuckChatWebViewFragment().apply {
807810
duckChatUrl?.let {
808811
arguments = Bundle().apply {
@@ -813,22 +816,41 @@ open class BrowserActivity : DuckDuckGoActivity() {
813816

814817
duckAiFragment = fragment
815818
val transaction = supportFragmentManager.beginTransaction()
816-
transaction.setCustomAnimations(
817-
com.duckduckgo.mobile.android.R.anim.slide_from_right,
818-
com.duckduckgo.mobile.android.R.anim.slide_to_right,
819-
)
820819
transaction.replace(binding.duckAiFragmentContainer.id, fragment)
821820
transaction.commit()
821+
822+
if (!wasFragmentVisible) {
823+
// If the fragment was already visible but needs to be force-reloaded, we don't want to animate it in again.
824+
animateDuckAiFragmentIn()
825+
}
822826
}
823827

824828
private fun restoreDuckChat(fragment: DuckChatWebViewFragment) {
829+
if (fragment.isVisible) {
830+
return
831+
}
832+
825833
val transaction = supportFragmentManager.beginTransaction()
826-
transaction.setCustomAnimations(
827-
com.duckduckgo.mobile.android.R.anim.slide_from_right,
828-
com.duckduckgo.mobile.android.R.anim.slide_to_right,
829-
)
830834
transaction.show(fragment)
831835
transaction.commit()
836+
837+
animateDuckAiFragmentIn()
838+
}
839+
840+
private fun animateDuckAiFragmentIn() {
841+
val duckAiContainer = binding.duckAiFragmentContainer
842+
val browserContainer = if (swipingTabsFeature.isEnabled) binding.tabPager else binding.fragmentContainer
843+
844+
duckAiContainer.slideAndFadeInFromRight()
845+
browserContainer.slideAndFadeOutToLeft()
846+
}
847+
848+
private fun animateDuckAiFragmentOut(onComplete: () -> Unit) {
849+
val duckAiContainer = binding.duckAiFragmentContainer
850+
val browserContainer = if (swipingTabsFeature.isEnabled) binding.tabPager else binding.fragmentContainer
851+
852+
duckAiContainer.slideAndFadeOutToRight(onComplete)
853+
browserContainer.slideAndFadeInFromLeft()
832854
}
833855

834856
private fun configureOnBackPressedListener() {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.animations
18+
19+
import android.view.View
20+
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
21+
22+
private const val ANIMATION_DURATION = 300L
23+
private const val SLIDE_OFFSET_RATIO = 0.33f
24+
25+
fun View.slideAndFadeInFromLeft(onComplete: (() -> Unit)? = null) = startSlideAndFadeAnimation(
26+
translationXFrom = -width.toFloat() * SLIDE_OFFSET_RATIO,
27+
translationXTo = 0f,
28+
alphaFrom = 0f,
29+
alphaTo = 1.0f,
30+
onComplete = onComplete,
31+
)
32+
33+
fun View.slideAndFadeOutToLeft(onComplete: (() -> Unit)? = null) = startSlideAndFadeAnimation(
34+
translationXFrom = 0f,
35+
translationXTo = -width.toFloat() * SLIDE_OFFSET_RATIO,
36+
alphaFrom = 1f,
37+
alphaTo = 0f,
38+
onComplete = onComplete,
39+
)
40+
41+
fun View.slideAndFadeInFromRight(onComplete: (() -> Unit)? = null) = startSlideAndFadeAnimation(
42+
translationXFrom = width.toFloat() * SLIDE_OFFSET_RATIO,
43+
translationXTo = 0f,
44+
alphaFrom = 0f,
45+
alphaTo = 1.0f,
46+
onComplete = onComplete,
47+
)
48+
49+
fun View.slideAndFadeOutToRight(onComplete: (() -> Unit)? = null) = startSlideAndFadeAnimation(
50+
translationXFrom = 0f,
51+
translationXTo = width.toFloat() * SLIDE_OFFSET_RATIO,
52+
alphaFrom = 1f,
53+
alphaTo = 0.0f,
54+
onComplete = onComplete,
55+
)
56+
57+
private fun View.startSlideAndFadeAnimation(
58+
translationXFrom: Float,
59+
translationXTo: Float,
60+
alphaFrom: Float,
61+
alphaTo: Float,
62+
onComplete: (() -> Unit)? = null,
63+
) {
64+
translationX = translationXFrom
65+
alpha = alphaFrom
66+
67+
animate()
68+
.translationX(translationXTo)
69+
.alpha(alphaTo)
70+
.setDuration(ANIMATION_DURATION)
71+
.setInterpolator(FastOutSlowInInterpolator())
72+
.apply { onComplete?.let { withEndAction(it) } }
73+
.start()
74+
}

0 commit comments

Comments
 (0)