Skip to content

Instantly share code, notes, and snippets.

@claudijo
Last active July 24, 2023 03:52
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save claudijo/0cf9f43705efadfeb852 to your computer and use it in GitHub Desktop.
Save claudijo/0cf9f43705efadfeb852 to your computer and use it in GitHub Desktop.
An Android view for typewriting-like animations

TypewriterView

An Android view that can be used to sequentially animate typing and deleting one character at a time. Pausing and enqueuing arbitrary Runnables is also possible. Inspired by Devunwired @ Stackoverflow (see http://stackoverflow.com/questions/6700374/android-character-by-character-display-text-animation).

An EditText view has been used as base to get a blinking cursor. This has the drawback that the user could edit the text while it is animating. Minimum changes to the code are required to instead use a TextView as base if that is an issue.

Example

TypewriterView typewriterView = (TypewriterView) view.findViewById(R.id.typewriter);
typewriterView.setText("Hello ");
typewriterView.pause(3000)
                .type("cruel").pause()
                .delete("cruel").pause(500)
                .type("nice").pause()
                .type(" world").pause()
                .run(new Runnable() {
                    @Override
                    public void run() {
                        // Finalize the text if user fiddled with it during animation.
                        typewriterView.setText("Hello nice world");
                        typewriterView.setEnabled(false);
                    }
                });
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content">

    <your.package.TypewriterView
        android:id="@+id/tagline_typewriter"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="10dp"
        android:textSize="16sp"
        android:windowSoftInputMode="stateHidden"
        android:textColor="@android:color/white"
        android:text="@string/splash_tag_line_begining"/>

    <!-- Overlay a view to prevent user from focusing the typewriter edit text view -->
    <EditText
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:editable="false"
        android:focusable="false"
        android:background="@android:color/transparent"></EditText>

</FrameLayout>
import android.content.Context;
import android.graphics.Color;
import android.os.Handler;
import android.text.InputType;
import android.util.AttributeSet;
import android.widget.EditText;
import java.util.LinkedList;
import java.util.Queue;
/**
* Created by claudijo on 18/11/15. Inspired by Devunwired @ Stackoverflow, see
* http://stackoverflow.com/questions/6700374/android-character-by-character-display-text-animation
*/
public class TypewriterView extends EditText {
private boolean isRunning = false;
private long mTypeSpeed = 80;
private long mDeleteSpeed = 50;
private long mPauseDelay = 1000;
private Queue<Repeater> mRunnableQueue = new LinkedList<>();
private Runnable mRunNextRunnable = new Runnable() {
@Override
public void run() {
runNext();
}
};
public TypewriterView(Context context, AttributeSet attrs) {
super(context, attrs);
setBackgroundColor(Color.TRANSPARENT);
setCursorAtEnd();
setCursorVisible(true);
setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
}
public TypewriterView type(CharSequence text, long speed) {
mRunnableQueue.add(new TextAdder(text, speed, mRunNextRunnable));
if (!isRunning) runNext();
return this;
}
public TypewriterView type(CharSequence text) {
return type(text, mTypeSpeed);
}
public TypewriterView delete(CharSequence text, long speed) {
mRunnableQueue.add(new TextRemover(text, speed, mRunNextRunnable));
if (!isRunning) runNext();
return this;
}
public TypewriterView delete(CharSequence text) {
return delete(text, mDeleteSpeed);
}
public TypewriterView pause(long millis) {
mRunnableQueue.add(new TypePauser(millis, mRunNextRunnable));
if (!isRunning) runNext();
return this;
}
public TypewriterView run(Runnable runnable) {
mRunnableQueue.add(new TypeRunnable(runnable, mRunNextRunnable));
if (!isRunning) runNext();
return this;
}
public TypewriterView pause() {
return pause(mPauseDelay);
}
private void setCursorAtEnd() {
setSelection(getText().length());
}
private void runNext() {
isRunning = true;
Repeater next = mRunnableQueue.poll();
if (next == null) {
isRunning = false;
return;
}
next.run();
}
private abstract class Repeater implements Runnable {
protected Handler mHandler = new Handler();
private Runnable mDoneRunnable;
private long mDelay;
public Repeater(Runnable doneRunnable, long delay) {
mDoneRunnable = doneRunnable;
mDelay = delay;
}
protected void done() {
mDoneRunnable.run();
}
protected void delayAndRepeat() {
mHandler.postDelayed(this, mDelay);
}
}
private class TextAdder extends Repeater {
private CharSequence mTextToAdd;
public TextAdder(CharSequence textToAdd, long speed, Runnable doneRunnable) {
super(doneRunnable, speed);
mTextToAdd = textToAdd;
}
@Override
public void run() {
if (mTextToAdd.length() == 0) {
done();
return;
}
char first = mTextToAdd.charAt(0);
mTextToAdd = mTextToAdd.subSequence(1, mTextToAdd.length());
CharSequence text = getText();
setText(text.toString() + first);
setCursorAtEnd();
delayAndRepeat();
}
}
private class TextRemover extends Repeater {
private CharSequence mTextToRemove;
public TextRemover(CharSequence textToRemove, long speed, Runnable doneRunnable) {
super(doneRunnable, speed);
mTextToRemove = textToRemove;
}
@Override
public void run() {
if (mTextToRemove.length() == 0) {
done();
return;
}
char last = mTextToRemove.charAt(mTextToRemove.length() - 1);
mTextToRemove = mTextToRemove.subSequence(0, mTextToRemove.length() - 1);
CharSequence text = getText();
if (text.charAt(text.length() - 1) == last) {
setText(text.subSequence(0, text.length() - 1));
}
setCursorAtEnd();
delayAndRepeat();
}
}
private class TypePauser extends Repeater {
boolean hasPaused = false;
public TypePauser(long delay, Runnable doneRunnable) {
super(doneRunnable, delay);
}
@Override
public void run() {
if (hasPaused) {
done();
return;
}
hasPaused = true;
delayAndRepeat();
}
}
private class TypeRunnable extends Repeater {
Runnable mRunnable;
public TypeRunnable(Runnable runnable, Runnable doneRunnable) {
super(doneRunnable, 0);
mRunnable = runnable;
}
@Override
public void run() {
mRunnable.run();
done();
}
}
}
@kidronstav
Copy link

Is it possible to know when the typewriter finished the animation? I know I can calculate with the time-per-character but I'm not sure how or if there's a better way. cheers :)

@mousik
Copy link

mousik commented Dec 2, 2016

I have same question as @kidronstav

@yoryidan
Copy link

Probably it's a little bit late for the answer, but maybe it is useful for somebody.

You can get the length of the text already written in the typewriterView and compare it with the string you want to write.
Here, you can find an example using two strings that are declared in values/strings.xml:

if((R.string.string1).length()==typewriterView.getText().length()) {
//Finished writing text
}else{
//Text is not complete yet
}

@Eloren1
Copy link

Eloren1 commented Dec 13, 2020

Probably it's a little bit late for the answer, but maybe it is useful for somebody.

You can get the length of the text already written in the typewriterView and compare it with the string you want to write.
Here, you can find an example using two strings that are declared in values/strings.xml:

if((R.string.string1).length()==typewriterView.getText().length()) {
//Finished writing text
}else{
//Text is not complete yet
}

It's even much easier. We can just make isRunning public and then use it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment