From 0e0b39bd27ff82885a93622aaef02861ba7564ee Mon Sep 17 00:00:00 2001 From: bemusementpark <bemusementpark> Date: Thu, 8 Aug 2024 02:10:06 +0930 Subject: [PATCH 1/2] Fix text glitches to new line in TypeAnimationTextView --- .../common/ui/view/TypeAnimationTextView.kt | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/TypeAnimationTextView.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/TypeAnimationTextView.kt index f1ae11598310..2a6aa1c1528b 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/TypeAnimationTextView.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/TypeAnimationTextView.kt @@ -17,14 +17,22 @@ package com.duckduckgo.common.ui.view import android.content.Context +import android.graphics.Color +import android.text.Spannable +import android.text.SpannableString import android.text.Spanned +import android.text.style.ForegroundColorSpan import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView import com.duckduckgo.common.utils.extensions.html +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.text.BreakIterator import java.text.StringCharacterIterator import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.* @Suppress("NoHardcodedCoroutineDispatcher") class TypeAnimationTextView @JvmOverloads constructor( @@ -38,7 +46,6 @@ class TypeAnimationTextView @JvmOverloads constructor( private var typingAnimationJob: Job? = null private var delayAfterAnimationInMs: Long = 300 - private val breakIterator = BreakIterator.getCharacterInstance() var typingDelayInMs: Long = 20 var textInDialog: Spanned? = null @@ -48,7 +55,6 @@ class TypeAnimationTextView @JvmOverloads constructor( isCancellable: Boolean = true, afterAnimation: () -> Unit = {}, ) { - textInDialog = textDialog.html(context) if (isCancellable) { setOnClickListener { if (hasAnimationStarted()) { @@ -57,24 +63,29 @@ class TypeAnimationTextView @JvmOverloads constructor( } } } - if (typingAnimationJob?.isActive == true) typingAnimationJob?.cancel() - typingAnimationJob = launch { - textInDialog?.let { spanned -> - breakIterator.text = StringCharacterIterator(spanned.toString()) + typingAnimationJob?.cancel() - var nextIndex = breakIterator.next() - while (nextIndex != BreakIterator.DONE) { - text = spanned.subSequence(0, nextIndex) - nextIndex = breakIterator.next() + textInDialog = textDialog.html(context).let(::SpannableString).also { textInDialog -> + typingAnimationJob = launch { + val transparentSpan = ForegroundColorSpan(Color.TRANSPARENT) + breakSequence(textInDialog).forEach { index -> + text = textInDialog.apply { setSpan(transparentSpan, index, length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) } delay(typingDelayInMs) } + delay(delayAfterAnimationInMs) afterAnimation() } } } + private fun breakSequence(charSequence: CharSequence) = + BreakIterator.getCharacterInstance() + .apply { text = StringCharacterIterator(charSequence.toString()) } + .let { generateSequence { it.next() } } + .takeWhile { it != BreakIterator.DONE } + fun hasAnimationStarted() = typingAnimationJob?.isActive == true fun hasAnimationFinished() = typingAnimationJob?.isCompleted == true From 3ebfee9564902bd84501dd1d20db8fd30d138bf2 Mon Sep 17 00:00:00 2001 From: bemusementpark <bemusementpark> Date: Tue, 13 Aug 2024 17:20:54 +0930 Subject: [PATCH 2/2] Fix animation not completing --- .../app/onboarding/ui/page/WelcomePage.kt | 19 ++++++------- .../common/ui/view/TypeAnimationTextView.kt | 27 ++++++++++--------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt index 77afcadb0074..936784fb31fd 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt @@ -22,6 +22,7 @@ import android.app.Activity import android.content.Intent import android.graphics.Color import android.os.Bundle +import android.text.Spanned import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -77,7 +78,6 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p ViewModelProvider(this, viewModelFactory)[WelcomePageViewModel::class.java] } - private var ctaText: String = "" private var hikerAnimation: ViewPropertyAnimatorCompat? = null private var welcomeAnimation: ViewPropertyAnimatorCompat? = null private var typingAnimation: ViewPropertyAnimatorCompat? = null @@ -165,12 +165,11 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p viewModel.onDialogShown(onboardingDialogType) when (onboardingDialogType) { INITIAL -> { - ctaText = it.getString(R.string.preOnboardingDaxDialog1Title) + val ctaText = it.getString(R.string.preOnboardingDaxDialog1Title) binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) - binding.daxDialogCta.dialogTextCta.textInDialog = ctaText.html(it) binding.daxDialogCta.daxDialogContentImage.gone() - scheduleTypingAnimation { + scheduleTypingAnimation(ctaText) { binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingDaxDialog1Button) binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(INITIAL) } ViewCompat.animate(binding.daxDialogCta.primaryCta).alpha(MAX_ALPHA).duration = ANIMATION_DURATION @@ -180,14 +179,13 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p COMPARISON_CHART -> { binding.daxDialogCta.dialogTextCta.text = "" TransitionManager.beginDelayedTransition(binding.daxDialogCta.cardView, AutoTransition()) - ctaText = it.getString(R.string.preOnboardingDaxDialog2Title) + val ctaText = it.getString(R.string.preOnboardingDaxDialog2Title) binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) - binding.daxDialogCta.dialogTextCta.textInDialog = ctaText.html(it) binding.daxDialogCta.primaryCta.alpha = MIN_ALPHA binding.daxDialogCta.comparisonChart.root.show() binding.daxDialogCta.comparisonChart.root.alpha = MIN_ALPHA - scheduleTypingAnimation { + scheduleTypingAnimation(ctaText) { binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingDaxDialog2Button) binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(COMPARISON_CHART) } ViewCompat.animate(binding.daxDialogCta.primaryCta).alpha(MAX_ALPHA).duration = ANIMATION_DURATION @@ -199,15 +197,14 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p binding.daxDialogCta.dialogTextCta.text = "" binding.daxDialogCta.comparisonChart.root.gone() binding.daxDialogCta.primaryCta.alpha = MIN_ALPHA - ctaText = it.getString(R.string.preOnboardingDaxDialog3Title) + val ctaText = it.getString(R.string.preOnboardingDaxDialog3Title) binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) - binding.daxDialogCta.dialogTextCta.textInDialog = ctaText.html(it) binding.daxDialogCta.daxDialogContentImage.alpha = MIN_ALPHA binding.daxDialogCta.daxDialogContentImage.show() binding.daxDialogCta.daxDialogContentImage.setImageResource(R.drawable.ic_success_128) launchKonfetti() - scheduleTypingAnimation { + scheduleTypingAnimation(ctaText) { ViewCompat.animate(binding.daxDialogCta.daxDialogContentImage).alpha(MAX_ALPHA).duration = ANIMATION_DURATION binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingDaxDialog3Button) binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(CELEBRATION) } @@ -244,7 +241,7 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p } } - private fun scheduleTypingAnimation(afterAnimation: () -> Unit = {}) { + private fun scheduleTypingAnimation(ctaText: String, afterAnimation: () -> Unit = {}) { typingAnimation = ViewCompat.animate(binding.daxDialogCta.daxCtaContainer) .alpha(MAX_ALPHA) .setDuration(ANIMATION_DURATION) diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/TypeAnimationTextView.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/TypeAnimationTextView.kt index 2a6aa1c1528b..397eab6e79e3 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/TypeAnimationTextView.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/TypeAnimationTextView.kt @@ -48,13 +48,15 @@ class TypeAnimationTextView @JvmOverloads constructor( private var delayAfterAnimationInMs: Long = 300 var typingDelayInMs: Long = 20 - var textInDialog: Spanned? = null + private var completeText: Spanned? = null fun startTypingAnimation( - textDialog: String, + htmlText: String, isCancellable: Boolean = true, afterAnimation: () -> Unit = {}, ) { + completeText = htmlText.html(context) + if (isCancellable) { setOnClickListener { if (hasAnimationStarted()) { @@ -66,17 +68,16 @@ class TypeAnimationTextView @JvmOverloads constructor( typingAnimationJob?.cancel() - textInDialog = textDialog.html(context).let(::SpannableString).also { textInDialog -> - typingAnimationJob = launch { - val transparentSpan = ForegroundColorSpan(Color.TRANSPARENT) - breakSequence(textInDialog).forEach { index -> - text = textInDialog.apply { setSpan(transparentSpan, index, length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) } - delay(typingDelayInMs) - } - - delay(delayAfterAnimationInMs) - afterAnimation() + typingAnimationJob = launch { + val transparentSpan = ForegroundColorSpan(Color.TRANSPARENT) + val partialText = SpannableString(completeText) + breakSequence(partialText).forEach { index -> + text = partialText.apply { setSpan(transparentSpan, index, length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) } + delay(typingDelayInMs) } + + delay(delayAfterAnimationInMs) + afterAnimation() } } @@ -92,7 +93,7 @@ class TypeAnimationTextView @JvmOverloads constructor( fun finishAnimation() { cancelAnimation() - textInDialog?.let { text = it } + completeText?.let { text = it } } fun cancelAnimation() = typingAnimationJob?.cancel()