Wave animation in Android TextView

Learn wave animation in android textview with practical examples, diagrams, and best practices. Covers android, android-animation, textview development techniques with visual explanations.

Creating a Dynamic Wave Animation in Android TextView

Abstract wave pattern flowing over text characters

Learn how to implement a captivating wave animation effect on text within an Android TextView, enhancing UI engagement and visual appeal.

Adding subtle animations to your Android application can significantly improve the user experience and make your UI more engaging. A wave animation, where text characters appear to move up and down in a fluid motion, is a visually appealing effect that can draw attention to specific text elements. This article will guide you through the process of creating such an animation for an Android TextView, leveraging custom Shader and Animation techniques.

Understanding the Core Concept: Custom Shader

The key to achieving a wave effect on text lies in manipulating how the text is drawn. Android's TextView uses a Paint object to render its text. By setting a custom Shader on this Paint object, we can control the color and pattern used to draw the text. For a wave effect, we'll use a LinearGradient shader, but dynamically adjust its position over time to create the illusion of movement.

A LinearGradient defines a gradient of colors along a line. If we create a gradient that cycles through colors (e.g., transparent to a color and back to transparent), and then shift this gradient horizontally or vertically, it can simulate a wave. The challenge is to apply this gradient in a way that affects the text's appearance as if it's undulating.

flowchart TD
    A[TextView Render Text] --> B{Get Paint Object}
    B --> C{Create Custom Shader}
    C --> D{Apply Shader to Paint}
    D --> E{Invalidate TextView}
    E --> F[Redraw with Animated Shader]
    C -- periodically update --> C

Flowchart of applying a custom shader for text animation

Implementing the Wave Shader

To implement the wave, we'll extend TextView and override its onDraw method. Inside onDraw, we'll create a LinearGradient that spans the width of the text. The colors in this gradient will define the 'wave' itself. By continuously updating the matrix of this LinearGradient and invalidating the TextView, we can make the wave appear to move across the text.

Consider a simple gradient from transparent to a color and back. When this gradient is applied to text, only the colored part will be visible. By shifting this gradient's starting point, we can make the colored 'wave' move. We'll use a ValueAnimator to smoothly change the offset of our gradient over time.

import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.LinearGradient
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Shader
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView

class WaveTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

    private var mLinearGradient: LinearGradient? = null
    private var mGradientMatrix: Matrix? = null
    private var mTranslate = 0f
    private var mWaveWidth = 0f
    private var mAnimator: ValueAnimator? = null

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        if (mWaveWidth == 0f) {
            mWaveWidth = w.toFloat() / 2 // Half the width of the TextView
        }

        val colors = intArrayOf(
            currentTextColor, // Start color (text color)
            0x00FFFFFF,       // Transparent (wave trough)
            currentTextColor  // End color (text color)
        )
        val positions = floatArrayOf(0f, 0.5f, 1f)

        mLinearGradient = LinearGradient(
            -mWaveWidth, 0f, 0f, 0f, colors, positions, Shader.TileMode.CLAMP
        )
        paint.shader = mLinearGradient
        mGradientMatrix = Matrix()

        startWaveAnimation()
    }

    private fun startWaveAnimation() {
        mAnimator?.cancel()
        mAnimator = ValueAnimator.ofFloat(0f, mWaveWidth * 2).apply {
            duration = 1500 // Animation duration
            repeatMode = ValueAnimator.RESTART
            repeatCount = ValueAnimator.INFINITE
            addUpdateListener { animation ->
                mTranslate = animation.animatedValue as Float
                postInvalidateOnAnimation()
            }
            start()
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        mGradientMatrix?.let { matrix ->
            matrix.setTranslate(mTranslate, 0f)
            mLinearGradient?.setLocalMatrix(matrix)
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        mAnimator?.cancel()
        mAnimator = null
    }
}

Kotlin code for WaveTextView implementing a horizontal wave animation.

Integrating into Layouts and Customization

Once you have your WaveTextView class, you can use it in your XML layouts just like any other TextView. Remember to use the fully qualified class name.

To customize the wave, you can modify the colors and positions arrays in the LinearGradient constructor. For example, to make the wave more subtle, you could use colors closer to your text color. To change the direction of the wave, you can adjust the LinearGradient's start and end points (e.g., 0f, -mWaveWidth, 0f, 0f for a vertical wave, and adjust mTranslate accordingly).

For a more complex wave, you might consider using a BitmapShader with a pre-rendered wave pattern, or even a custom Shader that calculates sine waves directly, though this adds significant complexity.

<!-- activity_main.xml -->
<LinearLayout 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"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <com.example.yourapp.WaveTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello Wave!"
        android:textSize="48sp"
        android:textColor="#FF0000"
        android:textStyle="bold" />

</LinearLayout>

Using the WaveTextView in an Android XML layout.

1. Create the Custom TextView Class

Define a new Kotlin or Java class (e.g., WaveTextView.kt) that extends AppCompatTextView.

2. Override onSizeChanged

Initialize your LinearGradient and Matrix within the onSizeChanged method to ensure they are set up correctly after the view's dimensions are known.

3. Implement ValueAnimator

Create a ValueAnimator to continuously update the mTranslate value, which controls the gradient's position. Set it to repeat infinitely.

4. Override onDraw

In onDraw, apply the mTranslate value to the mGradientMatrix and then set this matrix to your mLinearGradient. Call super.onDraw(canvas) to draw the text with the updated shader.

5. Handle Lifecycle

Override onDetachedFromWindow() to cancel the ValueAnimator to prevent memory leaks and unnecessary processing when the view is no longer visible.

6. Use in XML Layout

Integrate your WaveTextView into your activity_main.xml or any other layout file using its fully qualified class name.