@@ -21,17 +21,23 @@ import android.content.Context
21
21
import android.graphics.Color
22
22
import android.graphics.drawable.ColorDrawable
23
23
import android.text.Editable
24
+ import android.transition.ChangeBounds
25
+ import android.transition.Fade
26
+ import android.transition.TransitionManager
27
+ import android.transition.TransitionSet
24
28
import android.util.AttributeSet
25
29
import android.view.KeyEvent
26
30
import android.view.View
27
31
import android.view.ViewGroup
32
+ import android.view.animation.OvershootInterpolator
28
33
import android.view.inputmethod.EditorInfo
29
34
import android.widget.FrameLayout
30
35
import android.widget.ImageView
31
36
import android.widget.ProgressBar
32
37
import android.widget.TextView
33
38
import androidx.appcompat.widget.Toolbar
34
39
import androidx.coordinatorlayout.widget.CoordinatorLayout
40
+ import androidx.core.transition.doOnEnd
35
41
import androidx.core.view.doOnLayout
36
42
import androidx.core.view.isInvisible
37
43
import androidx.core.view.isVisible
@@ -51,6 +57,7 @@ import com.duckduckgo.app.browser.databinding.IncludeFindInPageBinding
51
57
import com.duckduckgo.app.browser.omnibar.Omnibar.OmnibarTextState
52
58
import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode
53
59
import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode.CustomTab
60
+ import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode.NewTab
54
61
import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.ChangeCustomTabTitle
55
62
import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.DisableVoiceSearch
56
63
import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.HighlightOmnibarItem
@@ -70,6 +77,7 @@ import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.ViewState
70
77
import com.duckduckgo.app.browser.omnibar.animations.BrowserTrackersAnimatorHelper
71
78
import com.duckduckgo.app.browser.omnibar.animations.PrivacyShieldAnimationHelper
72
79
import com.duckduckgo.app.browser.omnibar.animations.TrackersAnimatorListener
80
+ import com.duckduckgo.app.browser.omnibar.animations.omnibaranimation.OmnibarAnimationManager
73
81
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition
74
82
import com.duckduckgo.app.browser.tabswitcher.TabSwitcherButton
75
83
import com.duckduckgo.app.browser.viewstate.LoadingViewState
@@ -80,6 +88,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel
80
88
import com.duckduckgo.app.trackerdetection.model.Entity
81
89
import com.duckduckgo.common.ui.DuckDuckGoActivity
82
90
import com.duckduckgo.common.ui.experiments.visual.AppPersonalityFeature
91
+ import com.duckduckgo.common.ui.experiments.visual.store.VisualDesignExperimentDataStore
83
92
import com.duckduckgo.common.ui.view.KeyboardAwareEditText
84
93
import com.duckduckgo.common.ui.view.KeyboardAwareEditText.ShowSuggestionsListener
85
94
import com.duckduckgo.common.ui.view.gone
@@ -92,6 +101,7 @@ import com.duckduckgo.common.utils.FragmentViewModelFactory
92
101
import com.duckduckgo.common.utils.extensions.replaceTextChangedListener
93
102
import com.duckduckgo.common.utils.text.TextChangedWatcher
94
103
import com.duckduckgo.di.scopes.FragmentScope
104
+ import com.duckduckgo.duckchat.api.DuckChat
95
105
import com.google.android.material.appbar.AppBarLayout
96
106
import javax.inject.Inject
97
107
import kotlinx.coroutines.flow.collectLatest
@@ -132,6 +142,17 @@ open class OmnibarLayout @JvmOverloads constructor(
132
142
data class LoadingStateChange (val loadingViewState : LoadingViewState ) : StateChange()
133
143
}
134
144
145
+ data class TransitionState (
146
+ val showClearButton : Boolean ,
147
+ val showVoiceSearch : Boolean ,
148
+ val showTabsMenu : Boolean ,
149
+ val showFireIcon : Boolean ,
150
+ val showBrowserMenu : Boolean ,
151
+ val showBrowserMenuHighlight : Boolean ,
152
+ val showChatMenu : Boolean ,
153
+ val showSpacer : Boolean ,
154
+ )
155
+
135
156
@Inject
136
157
lateinit var viewModelFactory: FragmentViewModelFactory
137
158
@@ -144,12 +165,23 @@ open class OmnibarLayout @JvmOverloads constructor(
144
165
@Inject
145
166
lateinit var pixel: Pixel
146
167
168
+ @Inject
169
+ lateinit var duckChat: DuckChat
170
+
147
171
@Inject
148
172
lateinit var dispatchers: DispatcherProvider
149
173
150
174
@Inject
151
175
lateinit var appPersonalityFeature: AppPersonalityFeature
152
176
177
+ @Inject
178
+ lateinit var visualDesignExperimentDataStore: VisualDesignExperimentDataStore
179
+
180
+ @Inject
181
+ lateinit var omnibarAnimationManager: OmnibarAnimationManager
182
+
183
+ private var previousTransitionState: TransitionState ? = null
184
+
153
185
private val lifecycleOwner: LifecycleOwner by lazy {
154
186
requireNotNull(findViewTreeLifecycleOwner())
155
187
}
@@ -171,14 +203,15 @@ open class OmnibarLayout @JvmOverloads constructor(
171
203
internal val omnibarTextInput: KeyboardAwareEditText by lazy { findViewById(R .id.omnibarTextInput) }
172
204
internal val tabsMenu: TabSwitcherButton by lazy { findViewById(R .id.tabsMenu) }
173
205
internal val fireIconMenu: FrameLayout by lazy { findViewById(R .id.fireIconMenu) }
206
+ internal val aiChatMenu: FrameLayout ? by lazy { findViewById(R .id.aiChatIconMenu) }
174
207
internal val browserMenu: FrameLayout by lazy { findViewById(R .id.browserMenu) }
175
208
internal val browserMenuHighlight: View by lazy { findViewById(R .id.browserMenuHighlight) }
176
209
internal val cookieDummyView: View by lazy { findViewById(R .id.cookieDummyView) }
177
210
internal val cookieAnimation: LottieAnimationView by lazy { findViewById(R .id.cookieAnimation) }
178
211
internal val sceneRoot: ViewGroup by lazy { findViewById(R .id.sceneRoot) }
179
212
internal val omniBarContainer: View by lazy { findViewById(R .id.omniBarContainer) }
180
213
internal val toolbar: Toolbar by lazy { findViewById(R .id.toolbar) }
181
- internal val toolbarContainer: View by lazy { findViewById(R .id.toolbarContainer) }
214
+ internal val toolbarContainer: ViewGroup by lazy { findViewById(R .id.toolbarContainer) }
182
215
internal val customTabToolbarContainer by lazy {
183
216
IncludeCustomTabToolbarBinding .bind(
184
217
findViewById(R .id.customTabToolbarContainer),
@@ -198,6 +231,30 @@ open class OmnibarLayout @JvmOverloads constructor(
198
231
internal val spacer: View by lazy { findViewById(R .id.spacer) }
199
232
internal val trackersAnimation: LottieAnimationView by lazy { findViewById(R .id.trackersAnimation) }
200
233
internal val duckPlayerIcon: ImageView by lazy { findViewById(R .id.duckPlayerIcon) }
234
+ internal val spacer1X: View ? by lazy { findViewById(R .id.spacer1X) }
235
+ internal val spacer2X: View ? by lazy { findViewById(R .id.spacer2X) }
236
+ internal val omniBarButtonTransitionSet: TransitionSet by lazy {
237
+ TransitionSet ().apply {
238
+ ordering = TransitionSet .ORDERING_TOGETHER
239
+ addTransition(
240
+ ChangeBounds ().apply {
241
+ duration = omnibarAnimationManager.getChangeBoundsDuration()
242
+ interpolator = OvershootInterpolator (omnibarAnimationManager.getTension())
243
+ },
244
+ )
245
+ addTransition(
246
+ Fade ().apply {
247
+ duration = omnibarAnimationManager.getFadeDuration()
248
+ addTarget(clearTextButton)
249
+ addTarget(voiceSearchButton)
250
+ addTarget(fireIconMenu)
251
+ addTarget(tabsMenu)
252
+ addTarget(aiChatMenu)
253
+ addTarget(browserMenu)
254
+ },
255
+ )
256
+ }
257
+ }
201
258
202
259
internal fun omnibarViews (): List <View > = listOf (
203
260
clearTextButton,
@@ -401,6 +458,9 @@ open class OmnibarLayout @JvmOverloads constructor(
401
458
browserMenu.setOnClickListener {
402
459
omnibarItemPressedListener?.onBrowserMenuPressed()
403
460
}
461
+ aiChatMenu?.setOnClickListener {
462
+ omnibarItemPressedListener?.onDuckChatButtonPressed()
463
+ }
404
464
shieldIcon.setOnClickListener {
405
465
if (isAttachedToWindow) {
406
466
viewModel.onPrivacyShieldButtonPressed()
@@ -428,7 +488,6 @@ open class OmnibarLayout @JvmOverloads constructor(
428
488
is CustomTab -> {
429
489
renderCustomTabMode(viewState, viewState.viewMode)
430
490
}
431
-
432
491
else -> {
433
492
renderBrowserMode(viewState)
434
493
}
@@ -439,7 +498,16 @@ open class OmnibarLayout @JvmOverloads constructor(
439
498
} else {
440
499
lastSeenPrivacyShield = null
441
500
}
442
- renderButtons(viewState)
501
+
502
+ if (visualDesignExperimentDataStore.experimentState.value.isEnabled) {
503
+ renderButtons(viewState)
504
+ } else {
505
+ renderAnimatedButtons(viewState)
506
+ }
507
+
508
+ omniBarButtonTransitionSet.doOnEnd {
509
+ omnibarTextInput.requestLayout()
510
+ }
443
511
}
444
512
445
513
open fun processCommand (command : OmnibarLayoutViewModel .Command ) {
@@ -548,6 +616,54 @@ open class OmnibarLayout @JvmOverloads constructor(
548
616
spacer.isVisible = viewState.showVoiceSearch && viewState.showClearButton
549
617
}
550
618
619
+ private fun renderAnimatedButtons (viewState : ViewState ) {
620
+ val newTransitionState = TransitionState (
621
+ showClearButton = viewState.showClearButton,
622
+ showVoiceSearch = viewState.showVoiceSearch,
623
+ showTabsMenu = viewState.showTabsMenu,
624
+ showFireIcon = viewState.showFireIcon,
625
+ showBrowserMenu = viewState.showBrowserMenu,
626
+ showBrowserMenuHighlight = viewState.showBrowserMenuHighlight,
627
+ showChatMenu = duckChat.showInAddressBar() && (viewState.showChatMenu || viewState.viewMode is NewTab ),
628
+ showSpacer = viewState.showClearButton || viewState.showVoiceSearch,
629
+ )
630
+
631
+ if (omnibarAnimationManager.isFeatureEnabled() &&
632
+ previousTransitionState != null &&
633
+ newTransitionState != previousTransitionState &&
634
+ ! viewState.isLoading
635
+ ) {
636
+ TransitionManager .beginDelayedTransition(toolbarContainer, omniBarButtonTransitionSet)
637
+ }
638
+
639
+ if (! newTransitionState.showVoiceSearch) {
640
+ clearTextButton.isInvisible = ! newTransitionState.showClearButton
641
+ spacer1X?.isVisible = newTransitionState.showSpacer
642
+ spacer2X?.isVisible = false
643
+ } else {
644
+ clearTextButton.isVisible = newTransitionState.showClearButton
645
+ if (newTransitionState.showClearButton) {
646
+ spacer2X?.isVisible = newTransitionState.showSpacer
647
+ spacer1X?.isVisible = false
648
+ } else {
649
+ spacer1X?.isVisible = newTransitionState.showSpacer
650
+ spacer2X?.isVisible = false
651
+ }
652
+ }
653
+ voiceSearchButton.isInvisible = ! newTransitionState.showVoiceSearch
654
+ tabsMenu.isVisible = newTransitionState.showTabsMenu
655
+ fireIconMenu.isVisible = newTransitionState.showFireIcon
656
+ browserMenu.isVisible = newTransitionState.showBrowserMenu
657
+ browserMenuHighlight.isVisible = newTransitionState.showBrowserMenuHighlight
658
+ aiChatMenu?.isVisible = newTransitionState.showChatMenu
659
+
660
+ if (omnibarAnimationManager.isFeatureEnabled()) {
661
+ toolbarContainer.requestLayout()
662
+ }
663
+
664
+ previousTransitionState = newTransitionState
665
+ }
666
+
551
667
private fun renderBrowserMode (viewState : ViewState ) {
552
668
renderOutline(viewState.hasFocus)
553
669
if (viewState.updateOmnibarText) {
@@ -572,6 +688,16 @@ open class OmnibarLayout @JvmOverloads constructor(
572
688
renderPulseAnimation(viewState)
573
689
574
690
renderLeadingIconState(viewState.leadingIconState)
691
+
692
+ renderHint(viewState)
693
+ }
694
+
695
+ private fun renderHint (viewState : ViewState ) {
696
+ if (viewState.viewMode is NewTab && duckChat.showInAddressBar()) {
697
+ omnibarTextInput.hint = context.getString(R .string.search)
698
+ } else {
699
+ omnibarTextInput.hint = context.getString(R .string.omnibarInputHint)
700
+ }
575
701
}
576
702
577
703
private fun renderCustomTabMode (
0 commit comments