Animate the text of a textView so that it can be displayed progressively?

3

I would like the textView text present in my activity not to be displayed all at once, but gradually, something like a Power Point transition.

An example of what I want to do exactly would be dialogue texts from the GBA pokemon games SEE FROM 1:33 not necessarily one character at a time as displayed in the video, but one word at a time until the end of the text.

I wonder if you can limit the total time of writing the text, so that if the tax limit is exceeded, the rest of the text is written instantly ( for very long texts ).

activity:

package genesysgeneration.font;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(android.R.layout.activity_list_item);

        tv=(TextView)findViewById(R.id.tv);
        tv.setText("Lorem Ipsum é simplesmente uma simulação de texto da indústria tipográfica e de impressos, e vem sendo utilizado desde o século XVI, quando um impressor desconhecido pegou uma bandeja de tipos e os embaralhou para fazer um livro de modelos de tipos. Lorem Ipsum sobreviveu não só a cinco séculos, como também ao salto para a editoração eletrônica, permanecendo essencialmente inalterado. Se popularizou na década de 60, quando a Letraset lançou decalques contendo passagens de Lorem Ipsum, e mais recentemente quando passou a ser integrado a softwares de editoração eletrônica como Aldus PageMaker.");

    }
}

xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

Edit:

I tried the suggested by @Mr_Anderson , but I did not succeed.

Multiple rows have errors:

MainActivity:

package genesysgeneration.pokemaos;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TypeWriter t = (TypeWriter)findViewById(R.id.meuTxt);
        t.setCharacterDelay(100);
        t.animateText("Olha só");

    }
}

Class:

package genesysgeneration.pokemaos;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;
import java.util.logging.Handler;

public class TypeWriter extends TextView {

    private CharSequence mText;
    private int mIndex;
    private long mDelay = 500;

    public TypeWriter(Context context){

        super(context);

    }

    public TypeWriter(Context context, AttributeSet attrs){

        super(context, attrs);

    }

    private Handler mHandler = new Handler();
    private Runnable characterAdder = new Runnable() {
        @Override
        public void run() {
            setText(mText.subSequence(0, mIndex++));
            if (mIndex<=mText.length()){

                mHandler.postDelayed(characterAdder, mDelay);

            }
        }
    };

    public void animateText(CharSequence text){

        mText=text;
        mIndex=0;

        setText("");
        mHandler.removeCallbacks(characterAdder);
        mHandler.postDelayed(characterAdder, mDelay);

    }

    public void setCharacterDelay(long millis){

        mDelay=millis;

    }

}

xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="genesysgeneration.pokemaos.MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.Pokemaos.view.custom.TypeWriter

        android:id="@+id/meuTxt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</RelativeLayout>
    
asked by anonymous 20.03.2017 / 22:36

3 answers

2

You will create a class that is a custom view

public class Typewriter extends TextView {

    private CharSequence mText;
    private int mIndex;
    private long mDelay = 500; //Default 500ms delay


    public Typewriter(Context context) {
        super(context);
    }

    public Typewriter(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private Handler mHandler = new Handler();
    private Runnable characterAdder = new Runnable() {
        @Override
        public void run() {
            setText(mText.subSequence(0, mIndex++));
            if(mIndex <= mText.length()) {
                mHandler.postDelayed(characterAdder, mDelay);
            }
        }
    };

    public void animateText(CharSequence text) {
        mText = text;
        mIndex = 0;

        setText("");
        mHandler.removeCallbacks(characterAdder);
        mHandler.postDelayed(characterAdder, mDelay);
    }

    public void setCharacterDelay(long millis) {
        mDelay = millis;
    }
}

Then use it in activity_main xml (Edited)

<genesysgeneration.pokemaos.Typewriter
        android:id="@+id/meuTxt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

Then just instantiate in MainActivity

Typewriter t = (Typewriter) findViewById(R.id.meuTxt);
t.setCharacterDelay(100);
t.animateText("Olha só que legal");
    
21.03.2017 / 15:05
3

Using the SDK's Property animation system is achieve the effect you want.

This approach allows you to "tassle" all the features of the animation system such as pause / resume , reverse , repeat , animator listener and update listener .

Type a wrapper on a ValueAnimator in conjunction with a TimeInterpolator and two TypeEvaluator .

The TimeInterpolator is used to calculate the number of letters or number of words that the text must have at a certain point in the animation.

The ValueAnimator uses the value calculated by the TimeInterpolator to determine the part of the text that should be displayed at this point in the animation.

TextViewAnimator.java

public class TextViewAnimator {

    private TextValueAnimator textValueAnimator;

    public static TextViewAnimator perLetter(TextView textView){

        int steps = textView.getText().length();
        TextViewAnimator textViewAnimator =
                new TextViewAnimator(textView,
                                     new TextEvaluatorPerLetter(),
                                     new TextInterpolator(steps));
        return textViewAnimator;
    }

    public static TextViewAnimator perWord(TextView textView){

        int steps = textView.getText().toString().split(" ").length;

        TextViewAnimator textViewAnimator =
                new TextViewAnimator(textView,
                                     new TextEvaluatorPerWord(),
                                     new TextInterpolator(steps));
        return textViewAnimator;
    }

    public TextViewAnimator(TextView textView,
                            TypeEvaluator typeEvaluator,
                            TextInterpolator textInterpolator){

        this.textValueAnimator = new TextValueAnimator(textView, textView.getText().toString());
        textValueAnimator.setEvaluator(typeEvaluator);
        textValueAnimator.setInterpolator(textInterpolator);
    }

    private static class TextValueAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {

        private WeakReference<TextView> weakTextView;

        public TextValueAnimator(TextView textView, String text) {

            weakTextView = new WeakReference<>(textView);
            setObjectValues(text);
            addUpdateListener(this);
        }

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            String text = (String) animation.getAnimatedValue();
            TextView textView = weakTextView.get();
            if(textView != null) {
                textView.setText(text);
            }
        }
    }

    private static class TextEvaluatorPerLetter implements TypeEvaluator {

        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {
            int step = (int) fraction;
            return ((String) endValue).substring(0, step);
        }
    }

    private static class TextEvaluatorPerWord implements TypeEvaluator {

        private String[] words;
        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {

            int step = (int) fraction;
            if(words == null){
                words = ((String) endValue).split(" ");
            }
            String textAtStep = "";
            for (int i = 1; i <= step; i++) {
                textAtStep += words[i-1] + " ";
            }

            return textAtStep;
        }
    }

    private static class TextInterpolator implements TimeInterpolator {

        private int steps;
        public TextInterpolator(int steps) {

            this.steps = steps;
        }
        @Override
        public float getInterpolation(float input) {
            return input * steps;
        }
    }

    public void start(){
        textValueAnimator.start();
    }
    public void cancel(){
        textValueAnimator.cancel();
    }
    public void end(){
        textValueAnimator.end();
    }

    @RequiresApi(19)  
    public void pause(){
        textValueAnimator.pause();
    }
    @RequiresApi(19)
    public void resume(){
        textValueAnimator.resume();
    }
    @RequiresApi(19)
    public boolean isStarted(){
        return textValueAnimator.isStarted();
    }
    @RequiresApi(19)
    public float getAnimatedFraction(){
        return textValueAnimator.getAnimatedFraction();
    }
    public void setRepeatCount(int value){
        textValueAnimator.setRepeatCount(value);
    }
    public void setRepeatMode(int repeatMode){
        textValueAnimator.setRepeatMode(repeatMode);
    }
    public void setDuration(long duration){
        textValueAnimator.setDuration(duration);
    }
    public void setStartDelay(long startDelay){
        textValueAnimator.setStartDelay(startDelay);
    }
    public void addUpdateListener(ValueAnimator.AnimatorUpdateListener listener){
        textValueAnimator.addUpdateListener(listener);
    }
    public void removeUpdateListener(ValueAnimator.AnimatorUpdateListener listener){
        textValueAnimator.removeUpdateListener(listener);
    }
    public boolean isRunning(){
        return textValueAnimator.isRunning();
    }
    public void addListener(Animator.AnimatorListener listener){
        textValueAnimator.addListener(listener);
    }
    public void removeListener(Animator.AnimatorListener listener){
        textValueAnimator.removeListener(listener);
    }
}

The class provides two factory methods :

  • TextViewAnimator.perLetter() . Returns a TextViewAnimator that animates text, previously assigned to TextView, letter by letter.
  • TextViewAnimator.perWord() . Returns a TextViewAnimator that animates text, previously assigned to TextView, word for word.

The choice of a wrapper, for implementation, rather than inheritance is due to the need to "hide" some of the public methods of the ValueAnimator class.

Example letter-by-letter animation usage:

public class MainActivity extends AppCompatActivity {

    TextView textView;
    Button button;
    TextViewAnimator textViewAnimator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView) findViewById(R.id.text1);

        textViewAnimator = TextViewAnimator.perLetter(textView);
        textViewAnimator.setDuration(5000);
        textViewAnimator.setRepeatCount(2);
        textViewAnimator.setRepeatMode(ValueAnimator.REVERSE);

        button = (Button)findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                textViewAnimator.start();
            }
        });    
    }
}

The activity layout ( activity_main.xml ) should have a Button and a TextView with id's "@+id/button" and "@+id/text1" respectively.

Notes:
 - Requires minSdkVersion 11 .  - Some methods require minSdkVersion 19 .

    
28.03.2017 / 15:24
2

Although Ramaral has given a valid answer, I thought of another way it works for any version of Android.

I basically use Runnanble with a delay concatenating letter by letter or word by word.

  • animPerLetter() : letter by letter
  • animPerWord() : word for word

MainActivity:

TextAnimatedView textAnimatedView = (TextAnimatedView) findViewById(R.id.tv);
textAnimatedView.setCharacterDelay(150);
textAnimatedView.animPerWord("Desta forma vai funcionar como esperado");

In XML you use this way below:

<seu.pacote.TextAnimatedView
    android:id="@+id/tv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

See below how the class TextAnimatedView was:

public class TextAnimatedView extends TextView {

    private Handler handler = new Handler();
    private StringBuilder stringBuilder = new StringBuilder();
    private CharSequence text;
    private String[] arr;
    private int i;
    private long delay = 450;

    public TextAnimatedView(Context context) {
        super(context);
    }

    public TextAnimatedView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            setText(text.subSequence(0, i++));
            if (i <= text.length()) {
                handler.postDelayed(runnable, delay);
            }
        }
    };

    public void animPerLetter(CharSequence text) {
        this.text = text;
        i = 0;

        setText("");
        handler.removeCallbacks(runnable);
        handler.postDelayed(runnable, delay);
    }

    private Runnable runnablePerWord = new Runnable() {
        @Override
        public void run() {

            stringBuilder.append(arr[i++]).append(" ");
            setText(stringBuilder.toString());
            if (i < arr.length) {
                handler.postDelayed(runnablePerWord, delay);
            }
        }
    };

    public void animPerWord(CharSequence text) {
        this.text = text;
        i = 0;

        setText("");
        arr = text.toString().split(" ");
        handler.removeCallbacks(runnablePerWord);
        handler.postDelayed(runnablePerWord, delay);
    }

    public void setCharacterDelay(long delay) {
        this.delay = delay;
    }
}

A GIF is worth a thousand images.

    
28.03.2017 / 18:12