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()