Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix text glitches to new line in TypeAnimationTextView #4874

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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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) }
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -38,17 +46,17 @@ 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
private var completeText: Spanned? = null

fun startTypingAnimation(
textDialog: String,
htmlText: String,
isCancellable: Boolean = true,
afterAnimation: () -> Unit = {},
) {
textInDialog = textDialog.html(context)
completeText = htmlText.html(context)

if (isCancellable) {
setOnClickListener {
if (hasAnimationStarted()) {
Expand All @@ -57,31 +65,35 @@ 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()
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()
}
}

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

fun finishAnimation() {
cancelAnimation()
textInDialog?.let { text = it }
completeText?.let { text = it }
}

fun cancelAnimation() = typingAnimationJob?.cancel()
Expand Down