Skip to content

Commit

Permalink
Fix text glitches to new line in TypeAnimationTextView (#4874)
Browse files Browse the repository at this point in the history
Task: https://app.asana.com/0/1125189844152671/1208000645162922/f

Issue URL: #4869

### Description

There is a typing animation that adds each letter to the screen
sequentially. The last word of each line can jerk to the next line if
the word is long enough. On iOS the equivalent animation keeps
characters in position as they are revealed. On Android this animation
is more akin to what you would see when you are actually typing and you
type a long word at the end of a line. The word jerks to the next line.
This is fine when you are typing. But at speed an in an animation it
does not look good. This PR copies iOS style, and applies 100%
transparency to all letters that have not yet been "typed" in the
animation.

### Steps to test this PR

1. Install a fresh copy of the app
2. Do onboarding

_Feature 1_
- [ Change TypeAnimationTextView to progressively reveal each character
rather than progressively adding each character to the output sting ]

### UI changes

Before


[Screen_recording_20240807_211447.webm](https://github.com/user-attachments/assets/8e619a1f-5cc1-46ac-8080-e0d940aa3ed4)

After


[Screen_recording_20240808_021820.webm](https://github.com/user-attachments/assets/f49489f0-404e-41eb-af94-09f0bade0c9a)

---------

Co-authored-by: bemusementpark <bemusementpark>
  • Loading branch information
bemusementpark authored Sep 18, 2024
1 parent 6f8b7fc commit 4f73a17
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 29 deletions.
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 @@ -78,7 +79,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 @@ -166,12 +166,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 @@ -181,14 +180,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 @@ -200,15 +198,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 @@ -245,7 +242,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

0 comments on commit 4f73a17

Please sign in to comment.