SineView

2 June 2016

Aprovechando el post de cómo subir tu propia librería al repositorio jCenter, he decidido rescatar de uno de los últimos proyectos de Aluxion un componente gráfico que creé desde cero. Se trata de una vista que genera una función senoidal que puede utilizarse para dar feedback al usuario de que una operación se está llevando a cabo: SineView.

Vistas en Android

Los componentes gráficos en Android se dividen en dos grupos: View y ViewGroup. Sus propios nombres nos generan una idea de cuál es la función de cada uno. Mientras que View es el componente gráfico básico, ViewGroup es un conjunto o contenedor invisible de views.

viewGroup View
ViewGroup. View

En la documentación de Android Developers podemos comprobar que cualquier widget o elemento que usamos en nuestros layouts extienden la clase View. TextView, EditText, ImageView Button son ejemplos de widgets. Aunque cada uno tenga una función y unas propiedades diferentes, todos tienen cosas en común; y la más importante es que heredan de View.

El ciclo de vida de un View empieza con su creación (por código o por xml). Posteriormente, se determina el espacio y la localización de la misma; y, finalmente, se pinta. Esto se traduce en varios métodos:

  • Constructors <--> onFinishInflate()
  • onMeasure(int width, int height) <--> onSizeChanged(int w, int h, int oldw, int oldh)
  • onDraw(Canvas canvas)
  • onSaveInstanceState() <--> onRestoreInstanceState

SineView

SineView fue creado con el pretexto de ser una barra de progreso infinita. La idea era dar un feedback al usuario con un elemento de la UI mientras la app hacía operaciones en segundo plano que podían conllevar varios segundos. El hecho de recrear una forma de ola me hizo pensar en la fórmula matemática de la función trigonométrica seno.

La función seno puede ser representada en una gráfica con respecto al tiempo, utilizando la fórmula de una onda senoidal:

y = Amplitud * sen ( Frecuencia * x + Fase)
sine
Onda senoidal

En ella, un par de puntos (x,y) y los siguientes parámetros definen la onda:

  • Amplitud: distancia entre el eje X y el punto más alto de la onda
  • Frecuencia: frecuencia de una onda
  • Fase: desplazamiento con respecto al eje Y

Una vez claro el concepto, podemos traducir este comportamiento a una vista en Android:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    generatePath(canvas);
}

protected void generatePath(Canvas canvas) {
    mPath = new Path();
    float x = 0;
    float y = generateSinePoint(x);
    mPath.moveTo(x, y);
    for (; x < mRectF.width(); x += STEP_X) {
        y = generateSinePoint(x);
        mPath.lineTo(x + mRectF.left, y + mRectF.top + scaleY);
    }
    mPath.lineTo(measuredWidth, measuredHeight + Y_OFFSET);
    mPath.lineTo(0, measuredHeight + Y_OFFSET);
    mPath.close();
    canvas.drawPath(mPath, mPaint);
}

protected float generateSinePoint(float x) {
    return (float) (amplitude * Math.sin(x * frequency - phase));
}
  1. onDraw, donde se recibe el objeto Canvas, que es el encargado de "dibujar" elementos básicos (un rectángulo, una línea, texto, un bitmap) para construir la vista completa.
  2. generatePath recorre toda la anchura de la vista utilizando la variable estática STEP_X para definir la distancia entre puntos.
  3. El método generateSinePoint es la traducción literal de la formula de la onda.

El resultado final es el siguiente:

SineView
SineView

Generando Feedback al usuario: animación

Ya tenemos construida nuestra vista estática que pinta un seno en el layout. Pero el objetivo era utilizarlo como una barra de progreso, por lo que debemos animar la onda.

El parámetro fase nos indica la situación instantánea de una onda, desplazando cierta distancia hacia un lado o hacia otro. Si conseguimos crear pequeños pasos incrementales de la fase de nuestra onda, parecerá que se está moviendo en el tiempo.

Android, desde la API 11, añadió la clase ValueAnimator, que permite modificar propiedades de un View para hacer un efecto animado (aparecer, rotar, escalar, etc.). Una de las funciones que tiene esta clase es la de obtener los valores de una animación para utilizarlos de manera manual. De tal manera, podemos obtener un animator que nos sirva para animar una onda entre 0 - 2PI y completar así un paso de nuestra animación.

protected void setValueAnimator() {
    progressValueAnimator = ValueAnimator.ofFloat(0, (float) (2 * Math.PI));
    progressValueAnimator.setDuration(paramSineAnimTime);
    progressValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
    progressValueAnimator.setRepeatMode(ValueAnimator.RESTART);
    progressValueAnimator.setInterpolator(new LinearInterpolator());
}

Nuestro ValueAnimator nos proporcionará valores lineales (de ahí el interpolador lineal) entre 0 y 2PI con modo de repetición infinito, para que nunca pare (hasta que lo hagamos manualmente) y en modo repetición.

En cada iteración de la animación deberemos pintar la onda de nuevo con los valores actualizados. ValueAnimator nos proporciona la interfaz AnimatorUpdateListener, donde podremos modificar los parámetros de la onda y repintar.

@Override
public void onAnimationUpdate(ValueAnimator animation) {
    step = (float) animation.getAnimatedValue();
    calculatePhase();
    invalidate();
}

El método calculatePhase() modificará la fase de la onda en función de la variable step y que, posteriormente, invalidaremos con el método invalidate() para que la vista vuelva al proceso de medida y pintado.

SineView
SineView animado

Conclusiones

En este post hemos visto cómo crear una vista propia utilizando una función trigonométrica. Aquí podéis encontrar el repositorio con el código y, si queréis utilizarla en vuestros proyectos, sólo tendréis que añadir la siguiente dependencia en vuestro fichero build.gradle:

compile 'es.guiguegon:sineview:1.0.0'