Skip to content
Merged
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
1 change: 1 addition & 0 deletions duckchat/duckchat-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ dependencies {
exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
}
testImplementation AndroidX.lifecycle.runtime.testing
testImplementation AndroidX.archCore.testing

coreLibraryDesugaring Android.tools.desugarJdkLibs
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.duckduckgo.duckchat.impl.inputscreen.ui

import android.animation.ValueAnimator
import android.app.Activity
import android.content.Intent
import android.os.Bundle
Expand All @@ -25,10 +26,12 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.ui.DuckDuckGoFragment
import com.duckduckgo.common.ui.store.AppTheme
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.common.utils.extensions.hideKeyboard
import com.duckduckgo.common.utils.extensions.showKeyboard
Expand All @@ -39,8 +42,11 @@ import com.duckduckgo.duckchat.api.inputscreen.InputScreenActivityResultParams
import com.duckduckgo.duckchat.impl.R
import com.duckduckgo.duckchat.impl.databinding.FragmentInputScreenBinding
import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command
import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command.AnimateLogoToProgress
import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command.EditWithSelectedQuery
import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command.HideKeyboard
import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command.SetInputModeWidgetScrollPosition
import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command.SetLogoProgress
import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command.ShowKeyboard
import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command.SubmitChat
import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command.SubmitSearch
Expand Down Expand Up @@ -80,6 +86,9 @@ class InputScreenFragment : DuckDuckGoFragment(R.layout.fragment_input_screen) {
@Inject
lateinit var viewModelFactory: InputScreenViewModelFactory

@Inject
lateinit var appTheme: AppTheme

private val viewModel: InputScreenViewModel by lazy {
val params = requireActivity().intent.getActivityParams(InputScreenActivityParams::class.java)
val currentOmnibarText = params?.query ?: ""
Expand All @@ -91,11 +100,23 @@ class InputScreenFragment : DuckDuckGoFragment(R.layout.fragment_input_screen) {

private val pageChangeCallback = object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
viewModel.onPageSelected(position)
binding.inputModeWidget.selectTab(position)
}

override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
viewModel.onPageScrolled(position, positionOffset)
}

override fun onPageScrollStateChanged(state: Int) {
if (state == ViewPager2.SCROLL_STATE_IDLE) {
viewModel.onScrollStateIdle()
}
}
}

private lateinit var pagerAdapter: InputScreenPagerAdapter
private var logoAnimator: ValueAnimator? = null

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Expand All @@ -109,6 +130,7 @@ class InputScreenFragment : DuckDuckGoFragment(R.layout.fragment_input_screen) {
configureOmnibar()
configureVoice()
configureObservers()
configureLogoAnimation()

binding.inputModeWidget.init()

Expand Down Expand Up @@ -168,11 +190,15 @@ class InputScreenFragment : DuckDuckGoFragment(R.layout.fragment_input_screen) {
}.launchIn(lifecycleScope)

viewModel.visibilityState.onEach {
binding.ddgLogo.isVisible = if (binding.viewPager.currentItem == 0) {
val isSearchMode = binding.viewPager.currentItem == 0
binding.ddgLogoContainer.isVisible = if (isSearchMode) {
it.showSearchLogo
} else {
it.showChatLogo
}

binding.ddgLogo.progress = if (isSearchMode) 0f else 1f

binding.actionNewLine.isVisible = it.newLineButtonVisible
}.launchIn(lifecycleScope)
}
Expand All @@ -190,6 +216,9 @@ class InputScreenFragment : DuckDuckGoFragment(R.layout.fragment_input_screen) {
is SubmitChat -> submitChatQuery(command.query)
is ShowKeyboard -> showKeyboard(binding.inputModeWidget.inputField)
is HideKeyboard -> hideKeyboard(binding.inputModeWidget.inputField)
is SetInputModeWidgetScrollPosition -> binding.inputModeWidget.setScrollPosition(command.position, command.offset)
is SetLogoProgress -> setLogoProgress(command.targetProgress)
is AnimateLogoToProgress -> animateLogoToProgress(command.targetProgress)
}
}

Expand All @@ -213,23 +242,19 @@ class InputScreenFragment : DuckDuckGoFragment(R.layout.fragment_input_screen) {
binding.viewPager.setCurrentItem(0, true)
viewModel.onSearchSelected()
viewModel.onSearchInputTextChanged(binding.inputModeWidget.text)
binding.ddgLogo.apply {
setImageResource(com.duckduckgo.mobile.android.R.drawable.logo_full)
isVisible = viewModel.visibilityState.value.showSearchLogo
}
binding.ddgLogoContainer.isVisible = viewModel.visibilityState.value.showSearchLogo
}
onChatSelected = {
binding.viewPager.setCurrentItem(1, true)
viewModel.onChatSelected()
viewModel.onChatInputTextChanged(binding.inputModeWidget.text)
binding.ddgLogo.apply {
setImageResource(R.drawable.logo_full_ai)
binding.ddgLogoContainer.apply {
val showChatLogo = viewModel.visibilityState.value.showChatLogo
val showSearchLogo = viewModel.visibilityState.value.showSearchLogo
isVisible = showChatLogo
if (showChatLogo && !showSearchLogo) {
alpha = 0f
animate().alpha(1f).setDuration(200L).start()
animate().alpha(1f).setDuration(LOGO_FADE_DURATION).start()
}
}
}
Expand All @@ -248,6 +273,9 @@ class InputScreenFragment : DuckDuckGoFragment(R.layout.fragment_input_screen) {
onInputFieldClicked = {
viewModel.onInputFieldTouched()
}
onTabTapped = { index ->
viewModel.onTabTapped(index)
}
}

private fun submitChatQuery(query: String) {
Expand Down Expand Up @@ -282,12 +310,41 @@ class InputScreenFragment : DuckDuckGoFragment(R.layout.fragment_input_screen) {
}.launchIn(lifecycleScope)
}

private fun configureLogoAnimation() = with(binding.ddgLogo) {
setMinAndMaxFrame(0, LOGO_MAX_FRAME)
setAnimation(
if (appTheme.isLightModeEnabled()) {
R.raw.duckduckgo_ai_transition_light
} else {
R.raw.duckduckgo_ai_transition_dark
},
)
}

private fun setLogoProgress(targetProgress: Float) {
binding.ddgLogo.progress = targetProgress
}

private fun animateLogoToProgress(targetProgress: Float) {
logoAnimator?.cancel()
binding.ddgLogo.apply {
logoAnimator = ValueAnimator.ofFloat(progress, targetProgress).apply {
duration = LOGO_ANIMATION_DURATION
addUpdateListener { progress = it.animatedValue as Float }
start()
}
}
}

private fun exitInputScreen() {
hideKeyboard(binding.inputModeWidget.inputField)
requireActivity().supportFinishAfterTransition()
}

override fun onDestroyView() {
logoAnimator?.cancel()
logoAnimator = null
binding.ddgLogo.clearAnimation()
binding.viewPager.unregisterOnPageChangeCallback(pageChangeCallback)
super.onDestroyView()
}
Expand All @@ -296,4 +353,10 @@ class InputScreenFragment : DuckDuckGoFragment(R.layout.fragment_input_screen) {
super.onResume()
viewModel.onActivityResume()
}

companion object {
const val LOGO_ANIMATION_DURATION = 350L
const val LOGO_MAX_FRAME = 15
const val LOGO_FADE_DURATION = 200L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ sealed class Command {
data class SubmitChat(val query: String) : Command()
data object ShowKeyboard : Command()
data object HideKeyboard : Command()
data class SetInputModeWidgetScrollPosition(val position: Int, val offset: Float) : Command()
data class SetLogoProgress(val targetProgress: Float) : Command()
data class AnimateLogoToProgress(val targetProgress: Float) : Command()
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ class InputModeWidget @JvmOverloads constructor(
var onChatTextChanged: ((String) -> Unit)? = null
var onInputFieldClicked: (() -> Unit)? = null

var onTabTapped: ((index: Int) -> Unit)? = null

var text: String
get() = inputField.text.toString()
set(value) {
Expand Down Expand Up @@ -153,6 +155,22 @@ class InputModeWidget @JvmOverloads constructor(
inputField.setOnClickListener {
onInputFieldClicked?.invoke()
}
addTabClickListeners()
}

private fun addTabClickListeners() {
val tabStrip = inputModeSwitch.getChildAt(0) as? ViewGroup ?: return

repeat(inputModeSwitch.tabCount) { index ->
inputModeSwitch.getTabAt(index)?.let { tab ->
tabStrip.getChildAt(index)?.setOnClickListener {
onTabTapped?.invoke(index)
if (inputModeSwitch.selectedTabPosition != index) {
tab.select()
}
}
}
}
}

private fun configureInputBehavior() = with(inputField) {
Expand Down Expand Up @@ -285,6 +303,10 @@ class InputModeWidget @JvmOverloads constructor(
}
}

fun setScrollPosition(position: Int, positionOffset: Float) {
inputModeSwitch.setScrollPosition(position, positionOffset, false)
}

private fun fade(
view: View,
visible: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,13 @@ class InputScreenViewModel @AssistedInject constructor(
) : ViewModel() {

private var hasUserSeenHistoryIAM = false
private var isTapTransition = false

private val newTabPageHasContent = MutableStateFlow(false)
private val voiceServiceAvailable = MutableStateFlow(voiceSearchAvailability.isVoiceSearchAvailable)
private val voiceInputAllowed = MutableStateFlow(true)
private var userSelectedMode: UserSelectedMode = NONE
private var currentPagePosition: Int = 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have the userSelectedMode field, could that be re-used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to think about this a little, will fix in a follow-up.

private val _visibilityState = MutableStateFlow(
InputScreenVisibilityState(
voiceInputButtonVisible = voiceServiceAvailable.value && voiceInputAllowed.value,
Expand Down Expand Up @@ -445,6 +447,44 @@ class InputScreenViewModel @AssistedInject constructor(
userSelectedMode = SEARCH
}

fun onPageScrolled(position: Int, positionOffset: Float) {
if (!isTapTransition) {
val logoProgress = calculateLogoProgress(position, positionOffset)
command.value = Command.SetLogoProgress(logoProgress)
val widgetOffset = calculateInputModeWidgetScrollPosition(positionOffset)
command.value = Command.SetInputModeWidgetScrollPosition(position, widgetOffset)
}
}

private fun calculateLogoProgress(position: Int, positionOffset: Float): Float {
if (newTabPageHasContent.value) return 1f
return if (position == 0) positionOffset else 1f - positionOffset
}

private fun calculateInputModeWidgetScrollPosition(positionOffset: Float): Float {
return when {
positionOffset <= 0.5f -> positionOffset * positionOffset * 2f
else -> 1f - (1f - positionOffset) * (1f - positionOffset) * 2f
}
}

fun onTabTapped(index: Int) {
if (currentPagePosition != index) {
isTapTransition = true
if (!newTabPageHasContent.value) {
command.value = Command.AnimateLogoToProgress(index.toFloat())
}
}
}

fun onPageSelected(position: Int) {
currentPagePosition = position
}

fun onScrollStateIdle() {
isTapTransition = false
}

fun onSendButtonClicked() {
val pixelParams = inputScreenPixelsModeParam(isSearchMode = userSelectedMode == SEARCH)
pixel.fire(DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_FLOATING_SUBMIT_PRESSED, parameters = pixelParams)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,27 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/ddgLogo"
android:layout_width="@dimen/ntpDaxLogoIconWidth"
<FrameLayout
android:id="@+id/ddgLogoContainer"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/homeTabDdgLogoTopMargin"
android:paddingTop="@dimen/inputScreenContentTopOffset"
android:adjustViewBounds="true"
android:maxWidth="180dp"
android:maxHeight="180dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/inputModeWidget"
app:srcCompat="@drawable/logo_full" />
app:layout_constraintTop_toBottomOf="@id/inputModeWidget">

<com.airbnb.lottie.LottieAnimationView
android:id="@+id/ddgLogo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:maxWidth="180dp"
android:maxHeight="180dp"
app:lottie_autoPlay="false"
app:lottie_loop="false" />

</FrameLayout>

<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion duckchat/duckchat-impl/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<item name="android:background">@drawable/tab_layout_background</item>
<item name="tabIndicatorGravity">center</item>
<item name="tabIndicatorColor">?attr/daxColorInputModeIndicator</item>
<item name="tabIndicatorAnimationMode">elastic</item>
<item name="tabIndicatorAnimationMode">linear</item>
<item name="tabIndicatorFullWidth">true</item>
<item name="tabSelectedTextColor">?attr/daxColorPrimaryText</item>
<item name="tabTextColor">?attr/daxColorPrimaryText</item>
Expand Down
Loading
Loading