Recreando la app de Apple Music en Framer

Pasa de Cero a Héroe en Framer con este tutorial paso a paso (completo con videos, archivos descargables y consejos de diseño).

Tes Mat
39 min readApr 11, 2018

También hay versiones en 🇬🇧 inglés y 🇷🇺 ruso de este tutorial, así como una versión más antigua en 🇰🇷 coreano.

¿Sabes cómo puedes deslizar hacia abajo la pantalla “Ahora suena” en Apple Music para transformarla mágicamente en un mini-reproductor? Pensé que sería un buen desafío replicar esto en Framer.

Mientras trabajaba en eso, también hice que algunas pantallas fueran escroleables, (y paginables, porque de todos modos es fácil). ¿Y por qué no hacer que verdaderamente reproduzca música?

Aquí hay un video del prototipo terminado (haz clic en los enlaces para verlo o abrirlo):

👀 ver en tu browser — 🖥 abrir en Framer

Primeros pasos

Sin duda, es un prototipo bastante largo (más de 500 líneas), pero dado a que lo repasaremos detalladamente, es un excelente punto de inicio para novatos en Framer. Si nunca antes has usado Framer, es bueno que sepas que hay una versión de prueba gratis por 14 días. Descárgala para seguir adelante.

No tienes que saber nada sobre Framer antes de comenzar este tutorial. Dentro del post, brindaré enlaces a secciones relevantes en las guías para principiantes o “Get Started” de Framer. Los enlaces con un fondo gris tipo código — como esta layer — llevan a explicaciones en la documentación oficial.

Al final de cada sección, hay enlaces para 👀 ver el prototipo en Safari o 🖥 abrir el mismo directamente en Framer.

Las pantallas fueron creadas en Sketch, pero usaremos Framer Design para crear rápidamente el mini-reproductor.

Combinaremos distintas animaciones (con diversa duración) para pasar del reproductor de pantalla completa al mini-reproductor, y viceversa.

Otras cosas que aprenderemos a hacer en este tutorial:

  • Importar desde Sketch
  • Usar filtros para convertir capas a escala de grises o invertir sus colores
  • Desenfocar el fondo con el efecto Background Blur
  • Hacer que algunos elementos (barra de pestañas y barra de estado) aparezcan en todas las pantallas
  • Utilizar un módulo para crear un reproductor de música con barra de progreso, control de volumen así como lecturas de tiempo de reproducción y de tiempo restante
  • Usar una capa de texto (Text Layer)
  • Crear una función que usa recortes de JavaScript a fin de mostrar el día actual en esa capa de texto
  • Usar componentes de scroll, también dentro de otros componentes de scroll …
  • … y activar el bloqueo de dirección (Direction Lock) para que no sea posible desplazarlos al mismo tiempo
  • Envolver (wrapear) grupos enmascarados del archivo Sketch en un componente de scroll
  • Usar un componente de paginación (PageComponent)
  • Utilizar capas padre para ajustar el tamaño de las páginas de ese componente de paginación

1. Importar el archivo de Sketch

Este archivo de Sketch contiene las pantallas que necesitaremos para construir el prototipo.

Los cinco artboards en nuestro archivo de Sketch

A propósito, utilicé las nuevas fuentes SF Pro en este archivo.

Contiene cinco artboards:

  • La pantalla de la pestaña “Library” (Biblioteca), que también incluye las barras de estado y pestañas
  • La pantalla de la pestaña “For You” (Para ti)
  • Un artboard con una segunda tarjeta para la pantalla «For You»: Favourites Mix
  • Y otro artboard con la tercera tarjeta: Chill Mix
  • La pantalla “Ahora suena” (Now Playing)

La pantalla “Library” es en realidad mucho más alta. Su lista de Últimas agregadas está enmascarada y la puedes encontrar en la página de Símbolos.

Lo hice así para que sea más fácil editar los álbumes. Hice lo mismo con los Recently Played (Reproducidos recientemente) de la pantalla “For You”.

La página de Símbolos en Sketch, con las Últimas agregadas para “Library” y los Recently Played para “For You”

¡Empecemos!

Crea un nuevo proyecto en Framer y guárdalo (quizás llamándolo ‘Apple Music’). Luego importa el archivo de Sketch.

El diseño fue hecho a 1x, lo que significa que las pantallas de iPhone 8 miden 375 x 667 puntos de interface. Pero al importarlos a Framer a @2x se mostrarán a 750 x 1334 píxeles.

Importar el archivo a una resolución ‘retina’ de 2x, cambiar el dispositivo a ‘iPhone 8’, y abreviar ‘sketch’ a ‘$’

Ahora tendrás esta línea en la parte superior de tu proyecto:

sketch = Framer.Importer.load("imported/Apple%20Music@2x", scale: 1)

Si cambiamos el nombre de la variable sketch a $ tendremos que teclear menos en los próximos pasos …

$ = Framer.Importer.load("imported/Apple%20Music@2x", scale: 1)

… porque ahora podemos escribir, por ejemplo, $.Status_Bar en lugar de sketch.Status_Bar.

👀 ver el prototipo — 🖥 abrirlo en Framer

2. Hacer que la pantalla “Library” sea escroleable

En este momento, solo podemos ver la pantalla «Library», porque los demás artboards están a la derecha, fuera de la pantalla. (Con la misma distancia entre ellos que en el archivo de Sketch.)

Cuando escroleas hasta el final de la lista de capas (Layer Panel), verás que nuestro “Library” artboard (ahora una capa, por supuesto) contiene tres capas hijas: Status_Bar, Tabs y Library_content. (Las dos últimas con hijas propias a su vez).

“Library” y sus hijas en la lista de capas

Bueno, el contenido de la pantalla como tal está en Library_content, y usando la función wrap() del componente de scroll lo hacemos escroleable:

scroll_library = ScrollComponent.wrap $.Library_content

Nuestro nuevo componente de scroll, scroll_library, será desplazable por defecto en todas direcciones, inclusive hacia los lados. Pero eso se soluciona fácilmente desactivando el scrollHorizontal.

scroll_library.scrollHorizontal = no

El final de la página está oculto parcialmente por la barra de pestañas, por lo que deberíamos agregar un poco de contentInset (recuadro de contenido):

scroll_library.contentInset =
bottom: $.Tabs.height + 80

Utilicé la altura (height) de $.Tabs, agregando 80 puntos adicionales a fin de dejar espacio para el mini-reproductor.

👀 ver el prototipo — 🖥 abrirlo en Framer

3. Hacer que sólo la primera pestaña esté activa

Ahora, todas las pestañas son rojas, sin embargo, solo la primera debería serlo, mientras que las inactivas deberían ser de color gris.

Las había dejado rojas a propósito, ya que puedes modificar el color de capas en Framer. Y eliminar un color (usando grayscale o saturate) es especialmente fácil.

Usamos un bucle for…in para iterar por todas las hijas (children) de $.Tabs, reduciendo su saturación a cero, lo que las tornará grises.

Todavía estarán (un poco) demasiadas oscuras, pero al reducir también su opacidad (opacity) al 60%, llegarán al tono correcto de gris.

for tab in $.Tabs.children
tab.saturate = 0
tab.opacity = 0.6

Entonces podremos restablecer esas propiedades para $.Tab_Library, porque es nuestra primera pestaña, la que debe estar activa.

$.Tab_Library.saturate = 100
$.Tab_Library.opacity = 1
👀 ver el prototipo — 🖥 abrirlo en Framer

4. Hacer que la pantalla “For You” sea escroleable

La capa del artboard $.For_you está fuera de pantalla hacia la derecha, así que la traemos para acá cambiando su posición x:

$.For_you.x = 0

Para hacerla escroleable, la envolvemos, tal como hicimos con la pantalla “Library”.

scroll_for_you = ScrollComponent.wrap $.For_you

De nuevo, hacemos algunos ajustes al componente de scroll:

scroll_for_you.props = 
scrollHorizontal: no
contentInset:
bottom: $.Tabs.height + 40

(En lugar de escribir una línea distinta para cada propiedad, puedes establecerlas todas a la vez en props.)

👀 ver el prototipo — 🖥 abrirlo en Framer

5. Colocar la barra de estado y la de pestañas sobre todo lo demás

Te darás cuenta de que hemos perdido la barra de pestañas y la de estado. Esto es algo normal, ya que ambas son hijas del artboard “Library”.

Podemos sacarlas de $.Library dejándolas sin capa padre:

$.Status_Bar.parent = null
$.Tabs.parent = null

Cambiar su padre (parent) a null las hará… huérfanas, supongo. Ahora su padre es la pantalla del dispositivo, y además saltarán al tope de la lista de capas.

La barra de pestañas y la barra de estado en la lista de capas

¡Justo lo que queríamos!

Sin embargo, hay un problema con esto. Las demás capas que crearemos en los próximos pasos (un componente de scroll por acá, una cubierta gris transparente por allá) también se colocarán encima de las capas ya existentes.

Entonces, tendríamos que traer las barras de estado y pestañas al frente otra vez (y otra vez, y otra vez):

$.Status_Bar.bringToFront()
$.Tabs.bringToFront()

La solución: dejaremos sin padre las barras de estado y pestañas después de todo lo demás, colocando las líneas de código correspondientes al final de nuestro proyecto.

Así que hice un fold (pliegue) que contiene esto …

# Colocar la barra de estado y la barra de pestañas encima de todo
$.Status_Bar.parent = null
$.Tabs.parent = null

… y me aseguré de que permanezca al final del documento.

👀 ver el prototipo — 🖥 abrirlo en Framer

6. Hacer que la lista de álbumes “Recently Played” sea escroleable

Toda la pantalla “For You” se puede desplazar, pero eso no nos impide de hacer que parte de ella también sea escroleable.

La sección “Recently Played” contiene muchos más álbumes que los que podemos ver actualmente. Hagamos que pueda desplazarse de manera horizontal.

recentlyPlayed = ScrollComponent.wrap $.Recently_Played_albums
recentlyPlayed.props =
scrollVertical: no
contentInset:
right: 20

Estos 20 puntos de recuadro de contenido (contentInset) harán que el último álbum se alinee con el botón “See All”.

Ahora la lista “Recently Played” también es escroleable

Limitar el movimiento de desplazamiento

Aunque hay una cosita que debemos arreglar. Notarás que al escrolear a la izquierda o a la derecha, también podrás moverte inadvertidamente hacia arriba o abajo. Así no es como funciona en la app original.

Cuando comienzas a escrolear en una dirección determinada, se debería bloquear el desplazamiento hacia la otra dirección. Para esto, debemos habilitar el bloqueo de dirección (directionLock) en ambos componentes de scroll. Deberían verse así:

# Componente de scroll para todo el artboard
scroll_for_you = ScrollComponent.wrap $.For_you
scroll_for_you.props =
scrollHorizontal: no
contentInset:
bottom: $.Tabs.height + 40
directionLock: yes
# Componente de scroll para la sección Recently Played
recentlyPlayed = ScrollComponent.wrap $.Recently_Played_albums
recentlyPlayed.props =
scrollVertical: no
contentInset:
right: 20
directionLock: yes
👀 ver el prototipo — 🖥 abrirlo en Framer

7. Un componente de paginación para las tarjetas “New Music Mix”, “Favourites Mix” y “Chill Mix”

Queremos que sea posible deslizarse entre “New Music Mix”, “Favourites Mix” y “Chill Mix”, haciendo que una tarjeta siempre quede en el centro de la pantalla (paginable, también conocido como un carrusel). Para tal fin utilizaremos un componente de paginación (PageComponent).

mixes = new PageComponent
frame: $.New_Music_mix.frame # Reutilizando el marco
parent: $.For_you
scrollVertical: no
directionLock: yes

La propiedad frame(marco) contiene las dimensiones de una capa (ancho y alto) y también su ubicación (posiciones x y y), entonces de esta manera el componente de paginación ocupará el mismo espacio en la capa padre ($.For_you) que la tarjeta original.

Un componente de paginación para las tarjetas; es gris transparente porque todavía está vacío

Ahora podemos usar la función addPage() para agregar las tarjetas, así:

mixes.addPage $.New_Music_mix
mixes.addPage $.Favourites_mix
mixes.addPage $.Chill_mix
👀 ver el prototipo — 🖥 abrirlo en Framer

8. Mostrar partes de las otras tarjetas

Hay un pequeño detalle. Una parte de la segunda tarjeta ya debería estar visible, como una sutil señal (affordance) de que es posible deslizarse para verla completa. (Al igual que con los álbumes Recently Played, donde también aparece el tercer álbum asomándose a un lado.)

Entonces nuestras tarjetas deberían ser más pequeñas. Tenemos que cortar una porción del borde derecho de la primera, hacer que la tarjeta “Favourites Mix” sea más pequeña en ambos lados, y cortar un poco del lado izquierdo de “Chill Mix”. Podemos hacerlo colocando cada tarjeta dentro de otra capa que servirá como una máscara.

(A propósito, puedes borrar las líneas addPage() que usamos previamente.)

Primero, una envoltura para la primera tarjeta:

wrapper1 = new Layer
width: $.New_Music_mix.width - 15
height: $.New_Music_mix.height
backgroundColor: null
clip: yes

Reutilizamos la altura (height) de la carta, pero restamos 15 puntos de su grosor (width). Nos deshacemos del backgroundColor por defecto poniéndolo en null, y al activar clip, la capa actuará como una máscara.

Luego colocamos $.New_Music_mix dentro de nuestro wrapper:

$.New_Music_mix.parent = wrapper1
$.New_Music_mix.y = 0

Sin embargo, ahora debemos configurar su posición vertical. Anteriormente no era necesario porque addPage() corrige automáticamente las posiciones x e y.

Y ahora añadimos nuestra envoltura como una página al componente de paginación mixes.

mixes.addPage wrapper1
La tarjeta “New Music Mix” ahora está enmascarada por una capa padre

Con la segunda tarjeta, “Favourites Mix”, hacemos lo mismo:

wrapper2 = new Layer
width: $.Favourites_mix.width - 30 # Corte de ambos lados
height: $.Favourites_mix.height
backgroundColor: null
clip: yes
$.Favourites_mix.parent = wrapper2
$.Favourites_mix.y = 0 # Restablecer la posición y
$.Favourites_mix.x = -15 # Reposicionar
mixes.addPage wrapper2

Con una diferencia: La movemos 15 puntos a la izquierda.

De esta forma, su capa padre, wrapper2, cortará 15 puntos de su lado izquierdo y 15 puntos de su lado derecho.

Y la tercera carta, “Chill Mix”, pierde 15 puntos de su lado izquierdo:

wrapper3 = new Layer
width: $.Chill_mix.width - 15 # Corte desde el lado izquierdo
height: $.Favourites_mix.height
backgroundColor: null
clip: yes
$.Chill_mix.parent = wrapper3
$.Chill_mix.y = 0 # Restablecer la posición y
$.Chill_mix.x = -15 # Reposicionar
mixes.addPage wrapper3
👀 ver el prototipo — 🖥 abrirlo en Framer

9. Hacer dinámica la fecha del día

La parte superior de la pantalla “For You” muestra la fecha de hoy. Es una imagen, así que a menos que estés leyendo esto el 17 de junio de 2023 (que promete ser un sábado agradable), será incorrecto. Pero eso se puede solucionar fácilmente con una capa de texto y algunas líneas de código.

Añadir una capa de texto

Primero hagamos una capa de texto (textLayer) con el tamaño de fuente, peso, posición y color correctos. Y cuando eso esté listo, haremos que su texto sea dinámico con una función.

Nuestra capa de texto:

today = new TextLayer
text: "SATURDAY, JUNE 17"
fontSize: 13.5
color: "red"
parent: $.Header_For_You
x: $.Today_s_date.x
y: $.Today_s_date.y

No hace falta configurar su fontFamily (familia de fuentes) porque en tu Mac tendrá la misma fuente predeterminada que en iOS: San Francisco. El tamaño de la fuente (fontSize) parece ser 13.5 puntos. ( 27 píxeles.)

He usado rojo, "red", como color de texto contrastante (y temporal) para poder encontrar la ubicación correcta de manera más sencilla.

La fecha existente está en una capa distinta, $.Today_s_date, y su padre es $.Header_For_You. Al darle a nuestro Text Layer el mismo padre, podemos reutilizar la posición de $.Today_s_date.

Verás que la capa tiene que subir un poco. Quitando 7 píxeles de su posición y debería dejarla en el lugar correcto.

y: $.Today_s_date.y - 3.5

Una función que devuelve la fecha de hoy

Ahora, aquí está la función que nos dará el día actual en forma de cadena de texto:

todaysDate = ->
days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']

now = new Date()

dayOfTheWeekNumber = now.getDay() # = un número entre 0 y 6
monthNumber = now.getMonth() # = un número entre 0 y 11

theDateAsText = days[dayOfTheWeekNumber] + ", " + months[monthNumber] + " " + now.getDate()

return theDateAsText

Lo explicaré línea por línea.

La primera línea crea la función, todaysDate.

todaysDate = ->

La flecha -> dice: “Esta es una función, y las líneas siguientes deben ser ejecutadas cuando sea llamada.”

La primera línea de nuestra nueva función solo crea un array (objeto de tipo lista, una matriz), days, que contiene los nombres de los días de la semana …

days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']

… y la segunda línea hace lo mismo con los nombres de los meses.

months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']

Entonces creamos now, un objeto de fecha de JavaScript, usando new Date().

now = new Date()

No damos más información al constructor Date(), por lo que de forma predeterminada contendrá la fecha actual (así como la hora, hasta el milisegundo).

Un objeto Date viene con un montón de funciones incorporadas; usaremos tres de ellas:

  • getDay(), para obtener el día actual de la semana. Devuelve un número entre 0 y 6. Podrías pensar que el primer día de la semana sería el lunes (o el sábado)… pero en este caso, el 0 representa domingo.
  • getMonth(), para obtener el número del mes actual. No hay discusión aquí: el primero siempre será enero.
  • getDate(), para obtener el día del mes. Ésta no usa la numeración basada en cero (como las anteriores), por lo que simplemente comienza con 1.

Primero, obtenemos los números para el día de la semana y mes corrientes y los guardamos en las variables dayOfTheWeekNumber y monthNumber.

dayOfTheWeekNumber = now.getDay()        # = un número entre 0 y 6
monthNumber = now.getMonth() # = un número entre 0 y 11

Ahora podemos construir una cadena de texto que contenga todo.

theDateAsText = days[dayOfTheWeekNumber] + ", " + months[monthNumber] + " " + now.getDate()

Con la primera parte, days[dayOfTheWeekNumber], se selecciona el día de la semana correcto del array que creamos anteriormente, y con la segunda parte, months[monthNumber], se hace lo mismo con el nombre del mes.

Los unimos (con una coma y espacio, ", ", entre ellos), y pegamos el día del mes al final con now.getDate().

Y luego, la última línea de nuestra función devuelve esa cadena de texto con un return.

return theDateAsText

Cuando llamas la función y la imprimes print, de la siguiente forma …

print todaysDate()

… verás la fecha de hoy en la Consola.

Usar la función en la capa de texto

Ahora podemos usar todaysDate() para configurar el text de nuestra capa de texto.

today = new TextLayer
text: todaysDate()
fontSize: 13.5
color: "#929292"
parent: $.Header_For_You
x: $.Today_s_date.x
y: $.Today_s_date.y - 3.5
textTransform: "uppercase"

Hice dos cambios más: el color correcto del texto es en realidad "#929292", y con esta transformación de texto (textTransform), todo cambiará a mayúsculas.

Todo se ve bien, por lo que podemos ocultar la capa original configurando su visible a no:

$.Today_s_date.visible = no
👀 ver el prototipo — 🖥 abrirlo en Framer

Prefiero poner mis funciones al comienzo de un proyecto. Entonces, hice un fold separado llamado “Functions”, justo debajo de la importación desde Sketch.

10. Alternar entre las pestañas

Ahora que la pantalla “For You” también está lista, podemos hacer que sea posible alternar entre las dos pantallas.

Así que cuando tocamos la pestaña “Library”, su pantalla correspondiente debe hacerse visible, mientras que la pantalla “For You” debería quedar oculta.

$.Tab_Library.onTap ->
scroll_library.visible = yes
scroll_for_you.visible = no

Y debería suceder lo contrario al tocar la pestaña “For You”.

$.Tab_For_You.onTap ->
scroll_for_you.visible = yes
scroll_library.visible = no

A propósito, así es cómo puedes atar un evento a cualquier capa importada de forma rápida:

Pro tip: Se puede añadir un evento, animación, o estado a cualquier capa, haciendo clic en su nombre en la lista de capas

Pero la pestaña correcta también debe estar activa. Para eso agregamos las siguientes líneas a ambos manejadores de eventos:

# Hacer que todas las pestañas se vean de color gris
for tab in $.Tabs.children
tab.saturate = 0
tab.opacity = .6
# …salvo ésta
@saturate = 100
@opacity = 1

El bucle for…in hace que todas las pestañas se vuelvan grises, tal como lo hicimos antes, y las dos últimas líneas vuelven la pestaña actual roja de nuevo.

En aquellas dos últimas líneas estamos escribiendo en realidad lo siguiente:

this.saturate = 100
this.opacity = 1

Con ‘this’ siendo la pestaña que recibió el evento, es decir, la que se tocó. Pero en lugar de ‘this.’, también se puede escribir ‘@’.

Agregamos una línea más debajo de estos manejadores de eventos onTap, porque cuando arranca el prototipo, la pantalla “For You” debe quedar oculta:

# Ocultar la pantalla “For You” inicialmente
scroll_for_you.visible = no
👀 ver el prototipo — 🖥 abrirlo en Framer

11. La pantalla “Ahora suena”

El único artboard que aún no hemos usado es la pantalla “Ahora suena”. También es escroleable ya que también contiene la letra de la canción y una lista de las próximas canciones.

scroll_now_playing = new ScrollComponent
width: Screen.width
height: Screen.height - 33
y: 33
scrollHorizontal: no
directionLock: yes

El componente de scroll está colocado 33 puntos más abajo porque hay un espacio en la parte superior de la pantalla. La pantalla “Ahora suena” comienza en realidad 13 puntos por debajo de los 20 puntos de la barra de estado.

Y como está colocado más bajo también restamos esos mismos 33 puntos de Screen.height cuando establecemos su height.

Por cierto, así es como debería verse al final:

La otra pantalla en el fondo y el mango de arrastre señalan que puede deslizarse hacia abajo para volver

El bloqueo de dirección está habilitado porque no queremos que la pantalla se desplace cuando cambiemos el volumen de reproducción o saltemos a otro punto en la canción.

El componente de scroll para la pantalla “Ahora suena”

Ahora el artboard. Lo traemos para acá cambiando su x a 0, y lo agregamos a la capa de contenido (content) del componente de scroll. (Porque así es como se lo hace sin usar wrap().)

$.Now_Playing.x = 0
$.Now_Playing.parent = scroll_now_playing.content

Verás que hay un montón de espacio al final de la página.

Espacio extra al final de “Ahora suena”

Eso es a propósito. Debido a que ahora al establecer un valor negativo para el contentInset (para la parte inferior, bottom), el usuario podrá escrolear más allá del final de la página (esto se llama overdrag) sin ver la pantalla debajo.

Agrega estas líneas a las propiedades del componente de scroll:

    contentInset:
bottom: -100
Espacio adicional de “overdrag”

Ah, la barra de pestañas todavía está visible. La moveremos hacia abajo.

Agrega esta línea, preferiblemente más arriba en el código, dentro del fold The Tab Bar:

$.Tabs.y = Screen.height

Colocará la barra de pestañas justo debajo de la pantalla.

Más adelante, al pasar de la pantalla “Ahora suena” al mini-reproductor, la animaremos hacia arriba.

👀 ver el prototipo — 🖥 abrirlo en Framer

12. Una cubierta gris transparente detrás de la pantalla “Ahora suena”

La parte superior de la pantalla actual (“Library” o “For You”) aún debería estar visible debajo de la pantalla “Ahora suena”, oscurecida por una capa cubierta gris.

Esta superposición puede ser una capa simple del tamaño de la pantalla con un color de 50% negro transparente, como esta:

overlay = new Layer
frame: Screen.frame
backgroundColor: "rgba(0,0,0,0.5)"

Con la función placeBehind() la movemos debajo de la pantalla “Ahora suena”:

overlay.placeBehind scroll_now_playing

Faltan algunos detalles, sin embargo..

La pantalla “Ahora suena” debería tener esquinas redondeadas, y en efecto las tiene… pero no cuando escroleas hacia arriba.

La pantalla “Ahora suena” no tiene esquinas redondeadas

Y la pantalla en el fondo también debería verse como otra carta que está más abajo en la pila, así:

La pantalla “Ahora suena” y la que está debajo parecen cartas apiladas

¿Cómo redondear las esquinas? Es obvio: el componente de scroll necesita un poco de radio de borde (borderRadius), algo así como 10 puntos.

scroll_now_playing = new ScrollComponent
width: Screen.width
height: Screen.height - 33
y: 33
scrollHorizontal: no
directionLock: yes
contentInset:
bottom: -100
borderRadius:
topLeft: 10
topRight: 10

(Puedes establecer el radio del borde por separado para distintas esquinas. Para las esquinas inferiores, usa bottomRight y bottomLeft.)

Ahora, la pantalla que se encuentra debajo de la pantalla “Ahora suena”, scroll_library, también debería verse como una carta.

Le damos la misma cantidad de radio de borde y la movemos 20 puntos hacia abajo para que quede justo debajo de la barra de estado.

Tiene que encogerse un poco, pero solo horizontalmente: una scaleX (escala horizontal) de 93 por ciento parece ser la correcta.

scroll_library.props =
borderRadius: 10
y: 20
scaleX: 0.93

Debido a este fondo más oscuro, deberíamos tener una barra de estado light cuando la pantalla “Ahora suena” esté visible. Otro filtro al rescate: con una inversión (invert) de 100% la hacemos blanca.

$.Status_Bar.invert = 100
👀 ver el prototipo — 🖥 abrirlo en Framer

13. Reproducir música con el módulo ‘Framer Audio’

Benjamin den Boer, del equipo de Framer, creó un módulo que hace muy fácil agregar un reproductor de música a tu proyecto.

Descarga el módulo Framer Audio como un archivo ZIP:

Descomprímelo, busca el archivo audio.coffee (está en la carpeta ‘src’) y arrástralo a la ventana de tu proyecto.

Verás esta línea nueva en la parte superior de tu proyecto:

audio = require 'audio'

(Framer habrá copiado automáticamente el archivo a la carpeta “modules” de tu proyecto.)

Tal como se indica en la página del módulo en GitHub, cambiaremos la línea a:

{Audio, Slider} = require "audio"

Con este módulo, envuelves los botones de reproducir y pausa existentes para crear un reproductor de audio.

Probablemente no lo hayas notado, pero si has importado un botón “Play”. Era un grupo oculto en Sketch por lo que su visible está deshabilitado.

Vamos a mostrarlo.

$.Button_Play.visible = yes

Y ahora podemos envolver los botones con Audio.wrap():

audio = Audio.wrap($.Button_Play, $.Button_Pause)

El reproductor resultante, llamado audio, tendrá la misma posición que los botones y también su lugar en la jerarquía. Entonces el reproductor de audio ahora también es una capa hija de $.Now_Playing.

Necesitamos un poco de música. Puede ser un archivo en línea, por lo que usaremos este clip de 90 segundos de vista previa de Apple Music de la canción de Onuka.

audio = Audio.wrap($.Button_Play, $.Button_Pause)
audio.audio = "http://audio.itunes.apple.com/apple-assets-us-std-000001/AudioPreview30/v4/a2/3c/57/a23c57a3-09b2-4742-c720-8fa122ab826c/mzaf_6357632044803095145.plus.aac.ep.m4a"

¿Cómo se puede encontrar una vista previa como esa? Busqué en el catálogo de Apple Music con su herramienta en línea.

Y luego usé el inspector web de Safari para ver cuál archivo .m4a se descargaba cuando reproduje la música, y copié esa URL.

Me encanta esta canción, pero no tengo idea de qué se trata, aparte del hecho de que “misto” (місто) es “ciudad” en ucraniano.

Ya puedes reproducir la música. Pruébalo.
Toca el botón “Play”!

👀 ver el prototipo — 🖥 abrirlo en Framer

14. Animar la portada del álbum

Cuando se reproduce la música, la portada del álbum debe tener su tamaño completo, como está ahora. Y cuando la música se detiene, la portada se encoge (y también pierde la mayor parte de su sombra).

De paso, la sombra en la app original es más una versión borrosa de la portada. Pero como la nuestra es negra, lo mantendremos simple y usaremos una sombra.

Para animar entre estos dos estados usaremos, por supuesto, States.

Pero primero, tenemos que preparar algunas cosas.

Preparación

Más adelante, mostraremos esta misma portada muy pequeña en el mini-reproductor… y haremos desaparecer la pantalla “Ahora suena” completamente. Por eso deberíamos sacar la portada de su capa padre y ponerla directamente en el componente de scroll.

Esto es fácil de hacer, con una sola línea:

$.Album_Cover.parent = scroll_now_playing.content

Todavía está en el componente de scroll, pero ahora de forma independiente, como una hermana de la pantalla “Ahora suena”. (Y ni siquiera tuvimos que corregir su posición).

A continuación, tenemos que deshacernos de la sombra existente (estática). Lo hice como un grupo separado en Sketch, por lo que simplemente puedes ocultar la capa $.Album_Cover_shadow.

$.Album_Cover_shadow.visible = no

Crear los estados “playing” y “paused”

Ahora podemos definir los estados.

Cuando se reproduce la música, la portada del álbum debería verse así:

Cómo debería verse la portada cuando se está reproduciendo la música
  • Se muestra en la plenitud de sus 311 x 311 puntos — por lo que su scale (escala) será 1
  • El color de la sombra es 40% negro —"rgba(0,0,0,0.4)"
  • La sombra se proyecta hacia abajo — shadowY es 20 puntos
  • … pero también hacia afuera en todas las direcciones — una shadowSpread (extensión de la sombra) de 10 puntos
  • (No hay shadowX)
  • El desenfoque de la sombra también es alto — 50 puntos de desenfoque gaussiano (shadowBlur)

Y cuando la música está en pausa, debería verse así:

Cómo debería verse la portada cuando la música está en pausa
  • El álbum es 249 x 249 — lo que hace una scale de 0.8
  • La sombra es muy ligera: solo 10% negro — "rgba(0,0,0,0.1)"
  • Una shadowY de 19
  • Nada de shadowSpread
  • Una shadowBlur de 37 puntos

(La sombra en realidad será un 20% más pequeña, debido al cambio de escala.)

Para mantenerlo simple, llamaremos nuestros estados "playing" y "paused". Podemos definir ambos al mismo tiempo:

$.Album_Cover.states =
playing:
scale: 1
shadowType: "outer"
shadowColor: "rgba(0,0,0,0.4)"
shadowY: 20
shadowSpread: 10
shadowBlur: 50
frame: $.Album_Cover.frame
animationOptions:
time: 0.8
curve: Spring(damping: 0.60)
paused:
scale: 0.8
shadowType: "outer"
shadowColor: "rgba(0,0,0,0.1)"
shadowY: 19
shadowSpread: 0
shadowBlur: 37
frame: $.Album_Cover.frame
animationOptions:
time: 0.5

También he incluido el frame original de la capa en cada estado. Esto se debe a que más adelante agregaremos un tercer estado para el mini-reproductor en el que cambiaremos la posición.

Además incluí también unas animationOptions (opciones de animación):

  • Animar al estado "playing" lleva 0.8 segundos, pero parece más rápido porque termina con un rebote (amortiguado) suave.
  • No hay rebote al animar a "paused" (usamos la curva predeterminada de Bezier.ease), y la duración de esa animación es de 0.5 segundos.

Para probar las animaciones, podemos hacer un ciclo entre ellas desencadenando un stateCycle() con cada toque en la portada.

$.Album_Cover.onTap ->
this.stateCycle "paused", "playing"

(Al incluir sus nombres, se ignorará el estado "default".)

Prueba de la animación entre estados de la portada del álbum

Esto se ve bien.

Puedes borrar esos eventos onTap() por ahora, porque vamos a iniciar estas animaciones con el arranque y la parada de la música.

Con stateSwitch() se puede cambiar a un cierto estado sin animar. Usamos esta función para hacer "paused" el estado inicial.

$.Album_Cover.stateSwitch "paused"

Animar entre los estados cuando la música comienza y se detiene

Ahora, podríamos atar eventos onTap a los botones “Play” y “Pause” para activar estas animaciones, como por ejemplo …

$.Button_Play.onTap ->
$.Album_Cover.animate "playing"

… pero más adelante tendremos dos botones más: los del mini-reproductor.

Entonces lo haremos de manera diferente. Vamos a escuchar los eventos playing y pause del reproductor de audio.

El reproductor de audio contiene un objeto player, que es el elemento de audio HTML5 que reproduce la música. Y aparentemente, podemos agregar funciones a este objeto que se ejecutarán cuando ocurra un evento. Esto se hace creando una función en player con un on frente del nombre del evento.

Así que playing y pause se vuelven onplaying y onpause.

# Cuando la música comienza a reproducirse
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Cuando la música se detiene
audio.player.onpause = ->
$.Album_Cover.animate "paused"
👀 ver el prototipo — 🖥 abrirlo en Framer

15. Recrear la barra de progreso y contadores de tiempo

Agregar la barra de progreso

El módulo de Framer Audio utiliza sliders (controles deslizantes) para su barra de progreso y control de volumen.

Tú personalizas un SliderComponent hasta que se vea como lo deseas (o creas un slider en Design) y luego lo pasas al reproductor de audio.

Prueba este slider:

progressBar = new SliderComponent
width: 311
height: 3
backgroundColor: "#DBDBDB"
knobSize: 7
x: Align.center
y: 363
parent: $.Now_Playing

Es tan ancho como la portada del álbum, 311 puntos, y es delgado, solo 3 puntos de alto. El gris claro del control deslizante es "#DBDBDB".

El tamaño del mando, knobSize, también es bastante pequeño, solo 7 puntos.

Al hacer $.Now_Playing su parent, se encontrará donde pertenece: dentro de la pantalla “Ahora suena”. Usamos Align.center para centrarlo horizontalmente y lo colocamos 363 puntos desde la parte superior.

La parte izquierda de un SliderComponent es su relleno (fill), una capa hija, por lo que debemos establecer su color por separado:

progressBar.fill.backgroundColor = "#8C8C91"

Y lo mismo es cierto para el mando (knob), que recibe el mismo color que el relleno:

progressBar.knob.props = 
backgroundColor: progressBar.fill.backgroundColor
shadowColor: null

(Nos deshacemos de su sombra predeterminada, ajustando su shadowColor a null.)

Ahora, para activar el control deslizante, lo pasamos a la función showProgress() del reproductor de audio.

audio.showProgress progressBar

Agregar el contador de tiempo reproducido

Justo debajo de la barra de progreso, a la izquierda, queremos el contador de tiempo reproducido. Es el mismo proceso: creas una capa de texto y luego la pasas al reproductor de audio.

timePlayed = new TextLayer
fontSize: 14
color: progressBar.fill.backgroundColor
x: progressBar.x
y: progressBar.y + 5.5
parent: $.Now_Playing

Ella usa la fuente San Francisco por defecto (iOS y Mac) con un tamaño de 14, y queremos que sea colocada a 5.5 puntos por debajo del progressBar. El color del texto es igual al backgroundColor del fill de la barra de progreso.

Luego, para que se actualice cuando se reproduce la música, la pasas a la función showTime() del reproductor de audio.

audio.showTime timePlayed

Agregar el contador de tiempo restante

También hay un contador de tiempo restante, que cuenta hacia atrás.

Tiene las mismas propiedades de texto que el contador timePlayed, así que podemos copiarlo …

timeRemaining = timePlayed.copy()

… y luego cambiar algunas propiedades para moverlo a la derecha:

  • Su borde derecho, maxX, debe estar alineado con el del progressBar.
  • Su texto debe estar alineado a la derecha.
  • Y necesita un ancho fijo para que el texto pueda alinearse a la derecha.
timeRemaining.props =
textAlign: Align.right
width: 60
maxX: progressBar.maxX
parent: $.Now_Playing

La pasas al reproductor de audio con showTimeLeft().

audio.showTimeLeft timeRemaining

Notarás que ahora todo está ubicado justamente encima de $.Progress_bar, la capa original de Sketch, por lo que podemos ocultarla:

$.Progress_bar.visible = no

A propósito, ahora puedes ver cuándo se ha cargado la música. Cuando el contador de tiempo restante se cambia a la duración correcta, -1:29, significa que el archivo está descargado y listo para reproducir. Cuando esto se demora demasiado, puedes descargar el archivo .m4a y guardarlo dentro del proyecto (preferiblemente en una carpeta nueva llamada “sounds”). Por supuesto, tendrás que cambiar la URL a una local.

👀 ver el prototipo — 🖥 abrirlo en Framer

16. Recreando el control de volumen

Como era de esperar, el control de volumen también es un SliderComponent.

volumeSlider = new SliderComponent
width: 266
height: 3
backgroundColor: progressBar.backgroundColor
knobSize: 28
x: 50
y: 559
parent: $.Now_Playing
value: 0.75

El backgroundColor de este es el mismo que el del progressBar.

El volumen del sonido en el diseño está en el 75%, por lo que seguimos con este valor (value) para el SliderComponent.

Su fill también tiene el mismo color que el de progressBar.

volumeSlider.fill.backgroundColor = progressBar.fill.backgroundColor

El mando mantiene su color blanco predeterminado, pero tiene una sombra distinta, y tiene un borde muy fino de solo 0.5 puntos.

volumeSlider.knob.props = 
borderColor: "#ccc"
borderWidth: 0.5
shadowY: 3
shadowColor: "rgba(0,0,0,0.2)"
shadowBlur: 4

El control ahora debería tener el mismo aspecto que el diseñado en Sketch.

Antes de agregarlo, ponemos el volumen real del reproductor de audio también al 75%.

audio.player.volume = 0.75

Y igual a las funciones anteriores showProgress(), showTime() y showTimeLeft(), también hay una showVolume():

audio.showVolume volumeSlider

Ahora podemos hacer desaparecer la capa original:

$.Volume_slider.visible = no
👀 ver el prototipo — 🖥 abrirlo en Framer

17. Dibujar el mini-reproductor en Framer Design

Cuando arrastres la pantalla “Ahora suena” hacia abajo, debe convertirse a un pequeño mini-reproductor justo encima de la barra de pestañas.

Ese mini-reproductor no está en nuestro archivo de Sketch. Sin embargo, es muy sencillo: un fondo transparente con una mini versión de la portada del álbum, el título de la canción, y algunos botones.

Entonces… ¡tomaremos un descanso del code!
Teclea ⌘1 para cambiar a Design.

El lienzo de Design todavía estará vacío. Comienza con añadir un marco de un ‘Apple iPhone 8’.

Hice una captura de pantalla que sirve como plantilla para las dimensiones y posición correctas. Sólo tienes que arrastrarlo al marco.

Viene con un botón “Play” añadido porque vamos a necesitar uno de esos también.

Será demasiado grande debido a su resolución retina, pero, al igual que en Sketch (o en Framer Code), puedes teclear cálculos en los campos de propiedades. Por lo tanto, al cambiar su ancho de 750 a 750/2 asumirá el tamaño correcto.

Es recomendable bloquear la plantilla, para que no puedas seleccionarla o arrastrarla accidentalmente. Selecciónala y teclea ⌘L (o haz clic con el botón derecho y selecciona “Lock”).

Marcos y Formas

Anteriormente, todos los objetos dibujados en Design simplemente se convertían en capas, pero desde la Versión 107 tenemos Frames y Shapes.

En breve:

  • Los Shapes (formas) son para dibujar con precisión, y los Frames (marcos) son para diseño en general
  • Solo los Frames pueden tener restricciones de diseño (layout constraints) automáticas
  • Los Frames se convierten a layers en el mundo de código, pero los Shapes se transforman a algo nuevo: SVGLayers
  • O en la jerga HTML: los Frames son elementos <div> y los Shapes elementos <svg>

Para más información, mira este artículo de Framer Help.

Acerquémonos al botón “Play” y empecemos.

El botón “Play”

Necesitamos un triángulo. Puedes usar la herramienta de polígonos, crear uno de tres lados y rotarlo, pero probablemente será más fácil ir directamente a la herramienta Path. Te dará más control. (También puedes dibujar un polígono, hacer doble clic sobre él para convertirlo en un path y luego hacer correcciones).

Dale al triángulo un relleno negro.

El triángulo solo sería un botón pequeño y difícil de pulsar, así que lo haremos más grande dibujando un cuadrado encima. (Ahora ves por qué añadí esos contornos azules a la plantilla.)

Dibuja un Frame que siga el contorno azul. Debería tener un tamaño de 40 puntos.

Como es más grande (y no es un Shape o Path) se convertirá automáticamente en el padre del triángulo, que es exactamente lo que queremos.

Una capa padre más grande para crear un botón más grande

Cambia su nombre a Mini Button Play, y hazlo transparente desactivando su Fill.

El botón “Pause”

El botón “Pause” es bastante simple: dos rectángulos que tienen un poco de radio de borde.

Podrías crearlos con la herramienta Frame, pero es mejor usar la herramienta Rectangle, porque un Shape se puede posicionar en valores decimales. Te darás cuenta de que la posición y correcta para los rectángulos será 576.5 puntos.

El radio del borde parece ser ± 1 punto.

Un Shape se puede posicionar y dimensionar con mayor precisión que un Frame

Al igual que con el botón “Play”, ampliamos el área tocable dibujando un Frame encima, que llamaremos Mini Button Pause (y también lo hacemos transparente al desactivar su Fill).

El botón “Next”

Dos triángulos. Puedes ⌘D duplicar el triángulo de “Play” para tener algo con que comenzar.

En realidad, no vamos a usar este botón en el prototipo, pero podemos ordenar las cosas un poco al seleccionar estos dos y teclear ⌘↩ para ponerlos en un Frame (padre) …

Seleccionar “Add Frame” en el menú contextual

… que llamaremos Mini Button Next.

El título de la canción

El título de la canción está en SF Pro Text (o SF UI Text) de Apple, con un peso regular, 17 puntos de tamaño y un espaciado entre caracteres de -0.4.

La portada del álbum

Al pasar de la pantalla “Ahora suena” al mini-reproductor, la portada del álbum existente se encogerá, por lo que en realidad no necesitamos una portada en el mini-reproductor. Pero al dibujarla aquí en Design, tendremos su posición y sombra correctas a nuestra disposición en Code.

La imagen del álbum es 48 por 48 puntos y tiene un radio de borde de 3 puntos. Simplemente puedes dibujar un Frame y dejarlo en su azul transparente predeterminado. (Lo ocultaremos después de todos modos).

Su sombra debe ser 30% negra, con un desplazamiento en y de 3 puntos y un desenfoque de 10 puntos.

Llámalo Mini Album Cover.

El fondo del mini-reproductor

Necesitamos un Frame separado para el fondo del mini-reproductor, luego verás por qué.

El fondo debe ser de 375 por 64 puntos, y su color es un blanco muy claro, #F6F6F6, que es 50% opaco. Llámalo Mini Player Background.

Mini Player Background probablemente se convertirá en el padre de todos los demás objetos. Por lo tanto, es mejor seleccionar a sus hijos en la lista de capas y arrastrarlos afuera.

La línea en la parte superior

El mini-reproductor tiene una línea fina en su parte superior. Podrías dibujarla con la herramienta Path, pero es más fácil simplemente darle al Mini Player Background un borde superior. Debe tener 0.5 puntos de grosor y #AEAEAE como color.

La capa padre “Mini Player”

Ahora podemos seleccionar todos los objetos, hacer ⌘↩ “Add Frame” y nombrar el nuevo Frame abarcante Mini Player.

Establecer Targets

Necesitamos fijar Targets (objetivos) para los objetos que queremos usar en Code. Dale a cada uno un target haciendo clic sobre su círculo pequeño a la derecha:

  • Mini Player
  • Mini Album Cover
  • Mini Button Pause
  • Mini Button Play
  • Mini Button Background

Tu lista de capas ahora debería verse más o menos así:

Todo listo. Ya no necesitamos la plantilla, así que podemos hacer que Mini player.png sea invisible haciendo clic con el botón derecho sobre ella y seleccionando “Hide” (⌘;).

Tampoco necesitamos el fondo blanco (predeterminado) del Frame Apple iPhone 8, por lo que bajamos la opacidad de su Fill a 0%.

18. Afinar el mini-reproductor en Code

Cómo cambiaremos entre la pantalla “Ahora suena” y el mini-reproductor

Bueno, aquí está el truco. El mini-reproductor estará dentro de nuestra pantalla “Ahora suena” todo el tiempo. Simplemente lo ocultaremos cuando “Ahora suena” esté activa, así:

El mini-reproductor se esconde en la pantalla “Ahora suena”, y ella nunca se desplaza por completo fuera de la pantalla del dispositivo

La portada del álbum es una capa separada, como sabes, que redimensionamos y reposicionamos al realizar la transición entre los reproductores grande y pequeño.

Posicionar el mini-reproductor

Ponemos Mini_Player dentro del componente de scroll de la pantalla “Ahora suena” y cambiamos su y a cero para que se coloque en la parte superior.

Mini_Player.props =
parent: scroll_now_playing.content
y: 0
El mini-reproductor ahora está dentro del componente de scroll de la pantalla “Ahora suena”

Posicionar los botones “Play” y “Pause”

Deberíamos reorganizar los botones. El botón “Play” debería estar visible al principio, en el lugar donde está ahora el botón “Pause”.

Primero, le damos a Mini_Button_Play la misma posición horizontal que Mini_Button_Pause

Mini_Button_Play.x = Mini_Button_Pause.x

… y luego ocultamos Mini_Button_Pause.

Mini_Button_Pause.visible = no
El botón “Play” ahora tiene la posición correcta, y el botón “Pause” está oculto

Hacer que los botones “Play” y “Pause” reaccionen a toques

Haremos que estos botones también reproduzcan y pausen la música, usando las funciones play() y pause() (de HTML5-audio) en el player del reproductor de audio:

Mini_Button_Play.onTap ->
audio.player.play()
Mini_Button_Pause.onTap ->
audio.player.pause()

Ahora, podríamos usar estos mismos manejadores de eventos onTap para hacer que los botones aparezcan y desaparezcan (a fin de alternar entre los botones “Pause” y “Play”).

Pero ya estábamos escuchando algunos de los eventos del player que nos avisan cuando la música comienza o se detiene. Si lo recuerdas, los estábamos usando para aumentar y encoger la portada del álbum.

Regresa al fold Animating the Album Cover y agrega estas líneas:

# Cuando la música comienza a reproducirse
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Mostrar y ocultar los botones pequeños
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# Cuando la música se detiene
audio.player.onpause = ->
$.Album_Cover.animate "paused"
# Mostrar y ocultar los botones pequeños
Mini_Button_Play.visible = yes
Mini_Button_Pause.visible = no

De esta forma, los botones pequeños también cambiarán cuando toquemos los botones grandes en la pantalla “Ahora suena”.

Para hacer que funcionen también a la inversa (que cambien los botones grandes cuando tocamos los pequeños), agregamos líneas similares para $.Button_Play y $.Button_Pause.

# Cuando la música comienza a reproducirse
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Mostrar y ocultar los botones pequeños
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# … y también los botones grandes
$.Button_Play.visible = no
$.Button_Pause.visible = yes
# Cuando la música se detiene
audio.player.onpause = ->
$.Album_Cover.animate "paused"
# Mostrar y ocultar los botones pequeños
Mini_Button_Play.visible = yes
Mini_Button_Pause.visible = no
# … y también los botones grandes
$.Button_Play.visible = yes
$.Button_Pause.visible = no

Ahora todos los botones cambiarán al mismo tiempo, independientemente de cuál botón “Play” o “Pause” (grande o pequeño) haya sido pulsado.

👀 ver el prototipo — 🖥 abrirlo en Framer

19. Versión pequeña de la portada en el mini-reproductor

Ahora vamos a crear un estado adicional para la portada del álbum, en el que será pequeña y quedará colocada en el mini-reproductor, con la sombra adecuada.

Pero primero, como notaste al reproducir la música, la portada del álbum está detrás del mini-reproductor. Eso se arregla con un placeBefore():

$.Album_Cover.placeBefore Mini_Player

Ahora, para este nuevo estado, "mini", podemos copiar las propiedades de Mini_Album_Cover, esa pequeña portada de álbum que creamos en Design. Vamos a usar su frame, shadowColor, shadowY, y shadowBlur

$.Album_Cover.states.mini =
frame: Mini_Album_Cover.frame
shadowColor: Mini_Album_Cover.shadowColor
shadowY: Mini_Album_Cover.shadowY
shadowBlur: Mini_Album_Cover.shadowBlur
shadowSpread: Mini_Album_Cover.shadowSpread
scale: Mini_Album_Cover.scale

… pero también configuramos shadowSpread y scale, porque estas propiedades fueron cambiadas por los otros estados. (Mini_Album_Cover solo tiene los valores predeterminados: un shadowSpread de 0 y una scale de 1.)

En realidad, no queremos que Mini_Album_Cover sea visible; solo queríamos copiar sus propiedades para después ocultarla:

Mini_Album_Cover.visible = no

Ahora, a modo de prueba, puedes animar al nuevo estado "mini" dándole un toque en la portada:

$.Album_Cover.onTap ->
$.Album_Cover.animate "mini"
👀 ver el prototipo — 🖥 abrirlo en Framer

20. Transición de la pantalla “Ahora suena” al mini-reproductor

Todo se ve bien. Así podemos ocultar el mini-reproductor, por ahora.

Mini_Player.opacity = 0

Utilizamos la opacity porque vamos a querer animarlo.

Pero además no queremos que fuera pulsado accidentalmente cuando el usuario desliza la pantalla “Ahora suena” hacia abajo, por lo que también desactivaremos su visible.

Mini_Player.visible = no

Más adelante necesitaremos saber cuándo el mini-reproductor esté en uso (ya verás por qué). Creamos una variable para tal fin, miniPlayerActive, que en este punto todavía contendrá ‘no’.

miniPlayerActive = no

Escuchar el movimiento de scroll

Para saber cuándo el usuario ha arrastrado hacia abajo la pantalla “Ahora suena” vamos a escuchar a su evento onScrollEnd. Este evento desencadena cuando el usuario termina de escrolear.

scroll_now_playing.onScrollEnd ->

Ahora tenemos que comprobar si el usuario ha escroleado lo suficiente. Si no lo ha hecho, simplemente dejamos que el componente de scroll rebote.

En la aplicación original, el usuario tiene que arrastrar la pantalla “Ahora suena” 121 puntos o más desde la parte superior de la pantalla para hacer la transición al mini-reproductor.

La pantalla “Ahora suena” ya está a 33 puntos de la parte superior, por lo que comenzaremos nuestra animación cuando el usuario haya escroleado 88 puntos.

Pero como estamos escroleando hacia abajo, y no arriba como se lo haría normalmente (que también es la razón por la que hay cierta resistencia al deslizamiento), estamos chequeando por un valor negativo de scrollY, la distancia de desplazamiento.

(Puedes poner un print maxi_player.scrollY en el manejador de evento para comprobar esto.)

scroll_now_playing.onScrollEnd ->

if scroll_now_playing.scrollY < -88

Congelar la posición de desplazamiento

Ahora, cuando el usuario haya escroleado así de lejos, podemos comenzar la transición. Pero nos enfrentamos a un problema: la pantalla “Ahora suena” estará en un estado “desplazado hacia abajo”.

Resolveremos esto restableciendo rápidamente el componente de scroll a su estado inicial, así:

Antes de comenzar las animaciones, moveremos el componente de scroll hacia abajo mientras movemos su contenido hacia arriba. Lo hacemos al instante, sin animación.

Bueno, paso a paso:

# Hacer el componente de scroll saltar a la posición de su contenido
scroll_now_playing.y = scroll_now_playing.content.y + 33

(Nota que no estamos utilizando scrollY aquí, sino la posición y de la capa de contenido, content, que aumenta al escrolear hacia abajo).

Así que no importa qué distancia haya escroleado el usuario, nuestro componente siempre terminará en el lugar correcto.

Ahora movemos el contenido de vuelta a la parte superior:

    # … y restablecer el contenido a su posición inicial 
scroll_now_playing.scrollToPoint
y: 0
no

La función scrollToPoint() hace lo que dice: te permite escrolear hasta un cierto punto. Al establecer su argumento ‘animate’ en no, esto sucederá instantáneamente, sin animación.

Todo junto debería verse así:

scroll_now_playing.onScrollEnd ->

if scroll_now_playing.scrollY < -88 # 121 puntos menos 33

# Hacer el componente de scroll saltar
# a la posición de su contenido
scroll_now_playing.y = scroll_now_playing.content.y + 33

# … y restablecer el contenido a su posición inicial
scroll_now_playing.scrollToPoint
y: 0
no

Por favor pruébalo. Puedes escrolear hacia arriba y abajo todo lo que quieras, pero una vez que tiras lo suficiente hacia abajo, se mantendrá en el punto en que lo soltaste.

El desplazamiento se detiene una vez que se escrolea hacia abajo más de 88 puntos

Ahora prepárate. Tendremos nueve animaciones, con diferentes duraciones, todas corriendo al mismo tiempo.

Primer conjunto de animaciones

El primer conjunto de seis animaciones comienza inmediatamente, y la duración de todas ellas será de un tercio de segundo.

# -- Primer conjunto de animaciones: un tercio de segundo -- #
firstSetDuration = 0.3

En 0.3 segundos, vamos a:

  • Mostrar el mini-reproductor (opacity)
  • Ocultar la capa gris transparente detrás de él (opacity)
  • Restablecer la pantalla “Library” en el fondo (scaleX, y, borderRadius) …
  • … y hacer lo mismo para la pantalla “For You”
  • Hacer que la barra de estado vuelva a estar negra (invert)
  • Y mover la barra de pestañas hacia arriba (y)

Aquí vamos.

Mostrar el mini-reproductor: lo hacemos visible de nuevo y animamos su opacidad a 1.

Mini_Player.visible = yesMini_Player.animate
opacity: 1
options:
time: firstSetDuration

Ocultamos la cubierta gris transparente animando su opacidad a cero.

overlay.animate
opacity: 0
options:
time: firstSetDuration

A continuación, movemos scroll_library y scroll_for_you de nuevo a la parte superior de la pantalla, restablecemos su escala horizontal y quitamos su radio de borde.

scroll_library.animate
scaleX: 1
y: 0
borderRadius: 0
options:
time: firstSetDuration
scroll_for_you.animate
scaleX: 1
y: 0
borderRadius: 0
options:
time: firstSetDuration

(Al principio, solo hemos cambiado scroll_library, pero después de usar el prototipo, cualquiera de ellas podría estar en segundo plano.)

Anteriormente hicimos la barra de estado blanca cambiando su invert; ahora lo animamos de nuevo al valor predeterminado: 0.

$.Status_Bar.animate
invert: 0
options:
time: firstSetDuration

Al establecer la parte inferior de la barra de pestañas, su maxY, igual a la parte inferior de la pantalla (su height), la barra se deslizará hacia arriba.

$.Tabs.animate
maxY: Screen.height
options:
time: firstSetDuration

Debido a que usamos una variable, firstSetDuration, para establecer la duración de estas animaciones, podemos ralentizarlas para observar mejor lo que está sucediendo.

Por ejemplo, animar todo con una duración de 3 segundos …

# -- First set of animations, over a third of a second -- #
firstSetDuration = 0.3 * 10

… como lo hice para este GIF:

👀 ver el prototipo — 🖥 abrirlo en Framer

Segundo conjunto de animaciones

Las siguientes dos animaciones también comienzan de inmediato, pero son más lentas y tienen un rebote sutil.

# -- Segundo conjunto de animaciones: 0.7 segundos -- #
secondSetDuration = 0.7

En 0.7 segundos, vamos a:

  • Mover la pantalla completa de “Ahora suena” (que incluye el mini-reproductor) hacia abajo (y, borderRadius)
  • Hacer que la portada del álbum encaje en el mini-reproductor (una animación de estado)

No queremos animar todo fuera del marco del dispositivo porque el mini-reproductor aún debe estar visible, por lo que movemos la parte superior de la pantalla “Ahora suena” a la altura de la barra de pestañas + la altura del mini-reproductor.

scroll_now_playing.animate
y: Screen.height - $.Tabs.height - Mini_Player.height + 1
borderRadius:
topLeft: 0
topRight: 0
options:
time: secondSetDuration
curve: Spring(damping: 0.77)

(Aparentemente, tenemos que añadir 1 punto extra para que no aparezca una pequeña brecha.)

También nos deshacemos del radio de borde porque, de lo contrario, tendríamos un mini-reproductor con esquinas redondeadas.

La curva de resorte (Spring) añadida rebota solo ligeramente, con una amortiguación (damping) de 0.77 en lugar del 0.5 predeterminado.

Y utilizamos esta misma curva al reducir $.Album_Cover a su estado "mini":

$.Album_Cover.animate "mini",
time: secondSetDuration
curve: Spring(damping: 0.77)

No hemos incluido las opciones de animación cuando creamos "mini" (como lo hicimos para los estados "playing" y "paused"), pero podemos agregar la duración y la curva deseadas aquí.

Aquí hay un GIF de las ocho animaciones a una décima parte de su velocidad:

👀 ver el prototipo — 🖥 abrirlo en Framer

Última animación: ocultar la pantalla “Ahora suena”

Esta última animación comienza 0.5 segundos más tarde porque queremos asegurarnos de que el mini-reproductor esté en su lugar antes de desvanecer la pantalla detrás de él.

$.Now_Playing.animate
opacity: 0
options:
delay: 0.5
time: 0.5

(Aquí estamos animando la opacidad de la capa $.Now_Playing de Sketch que está dentro de nuestro componente de scroll.)

Desenfoque del fondo

Ahora que puedes ver la transparencia del mini-reproductor te darás cuenta que falta algo: desenfoque del fondo. Lo que está debajo del mini-reproductor debe mostrarse borroso.

Vuelve a Design, selecciona el mini-reproductor, dale un Blur de 25, y luego cambia este desenfoque de Layer a Background.

Aquí está el resultado:

👀 ver el prototipo — 🖥 abrirlo en Framer

Ah, y ahora que hicimos la transición al mini-reproductor también podemos “flip the switch”:

# El mini-reproductor está activo ahora
miniPlayerActive = yes

21. Transición desde el mini-reproductor de vuelta a “Ahora suena”

Ahora queremos volver. Cuando el usuario toca el mini-reproductor, debería brotar en la pantalla “Ahora suena”.

Escucharemos por un onTap en la capa de fondo del mini-reproductor.

Mini_Player_Background.onTap ->

¿Por qué el fondo? Porque de esta manera se puede usar los botones “Play” y “Pause” en el mini-reproductor sin también activar esta transición.

En el manejador del evento, partimos haciendo visible la pantalla “Ahora suena”:

# Muestra la pantalla “Ahora suena”
# para que no tenga que desvanecerse gradualmente
$.Now_Playing.opacity = 1

Está debajo del mini-reproductor de todos modos, y no queremos animar su transparencia mientras lo movemos hacia arriba.

Primer conjunto de animaciones

Ahora, nuestras animaciones. Hay un conjunto rápido (un tercio de segundo) y otro más lento (medio segundo). Primero el conjunto rápido:

# -- Primer conjunto de animaciones: un tercio de segundo -- #
firstSetDuration = 0.3

Ocultamos el mini-reproductor …

# Desvanecer el mini-reproductor
Mini_Player.animate
opacity: 0
options:
time: firstSetDuration

… y en los mismos 0.3 segundos bajamos la barra de pestañas::

# Bajar la barra de pestañas
$.Tabs.animate
y: Screen.height
options:
time: firstSetDuration

Es un movimiento pequeño de todos modos (en comparación con la pantalla completa “Ahora suena” deslizándose hacia arriba).

Aquí hay un GIF de lo que está sucediendo (también a una décima parte de la velocidad):

👀 ver el prototipo — 🖥 abrirlo en Framer

Segundo conjunto de animaciones

El segundo conjunto comienza al mismo tiempo, pero estas animaciones se ejecutan más lentamente: 0.5 segundos. Al igual que el primer conjunto, usan la curva predeterminada de Bezier.ease.

# -- Segundo conjunto de animaciones: medio segundo -- #
secondSetDuration = 0.5

La animación más obvia es la pantalla “Ahora suena” volviendo a subir:

# Animar el componente de scroll hacia arriba
scroll_now_playing.animate
y: 33
borderRadius:
topLeft: 10
topRight: 10
options:
time: secondSetDuration

(También restauramos el radio de borde en sus esquinas superiores.)

Y al mismo tiempo, queremos que la portada del álbum vuelva a su tamaño más grande. Pero, debemos verificar si la música se está reproduciendo, para que podamos animarla al estado correcto.

Como sabes, el objeto player en el reproductor de audio nos da acceso a su elemento de “audio” HTML5. Una de las propiedades de este elemento es paused, que será ‘true’ cuando la música no se está reproduciendo.

if audio.player.paused
$.Album_Cover.animate "paused",
time: secondSetDuration
else
$.Album_Cover.animate "playing",
time: secondSetDuration
curve: Bezier.ease

Al darles a estas animaciones un time, anularemos las duraciones que establecimos al crear los estados.

Tampoco queremos la curva de resorte contenida en "playing", por lo que la sobrescribimos con una curva Bezier.ease.

👀 ver el prototipo — 🖥 abrirlo en Framer

Lo que queda son las cosas que ocurren en el fondo, detrás de la pantalla “Ahora suena”:

  • La cubierta gris transparente debería volver a desvanecerse
  • La pantalla en el fondo también debería convertirse a una carta
  • La barra de estado deberá quedar blanca de nuevo

La cubierta gris:

# Mostrar la cubierta gris transparente
overlay.animate
opacity: 1
options:
time: secondSetDuration

Tratar con las pantallas “Library” y “For You”:

# Encoger y mover las pantallas en el fondo
scroll_library.animate
scaleX: 0.93
y: 20
borderRadius: 10
options:
time: secondSetDuration
scroll_for_you.animate
scaleX: 0.93
y: 20
borderRadius: 10
options:
time: secondSetDuration

(De nuevo, solo una de ellas será visible en este momento.)

La barra de estado:

# Hacer que la barra de estado sea blanca
$.Status_Bar.animate
invert: 100
options:
time: secondSetDuration

Todo listo. Ahora debemos hacer que el mini-reproductor sea intocable de modo que no puede ser activado inadvertidamente cuando el usuario desliza la pantalla hacia abajo …

Mini_Player.visible = no

… y registrar que el mini-reproductor no está activo.

miniPlayerActive = no
👀 ver el prototipo — 🖥 abrirlo en Framer

Prevenir las animaciones de la portada del álbum cuando el mini-reproductor está activo

A estas alturas, quizás te preguntes por qué necesitamos esta variable miniPlayerActive.

Bien, toca el botón “Play” en el mini-reproductor.

La portada del álbum crece y se contrae, tal como se le enseñaron

Estas animaciones no deberían suceder cuando estamos en el mini-reproductor.

Vuelve al fold # Animating the Album Cover.

Hasta ahora las funciones onplaying() y onpause() se veían así:

# Cuando la música comienza a reproducirse
audio.player.onplaying = ->
$.Album_Cover.animate "playing"
# Mostrar y ocultar los botones pequeños
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# … y también los botones grandes
$.Button_Play.visible = no
$.Button_Pause.visible = yes

(La función onpause() contendrá código parecido.)

Con una línea if adicional revisaremos miniPlayerActive, y solo cuando no esté activo cambiaremos el estado de $.Album_Cover.

# Cuando la música comienza a reproducirse
audio.player.onplaying = ->
if miniPlayerActive is no
$.Album_Cover.animate "playing"
# Mostrar y ocultar los botones pequeños
Mini_Button_Play.visible = no
Mini_Button_Pause.visible = yes
# … y también los botones grandes
$.Button_Play.visible = no
$.Button_Pause.visible = yes

(Agrega la misma línea a la función onpause().)

👀 ver el prototipo — 🖥 abrirlo en Framer

¡Hecho!

Espero que te 👏 haya gustado este tutorial.

Si es así, echa un vistazo a mi libro. Tiene tutoriales similares para otras dos apps más y mucho más sobre Framer Code. ¡Además hay una versión de vista previa gratuita!

The Framer book

--

--