¿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):
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.
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”.
¡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.
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
.
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).
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.
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
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
.)
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.
¡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.
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”.
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
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.
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
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 sí 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
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 # Reposicionarmixes.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 # Reposicionarmixes.addPage wrapper3
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 entre0
y6
. Podrías pensar que el primer día de la semana sería el lunes (o el sábado)… pero en este caso, el0
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 con1
.
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
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:
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
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:
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.
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.
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
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.
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.
Y la pantalla en el fondo también debería verse como otra carta que está más abajo en la pila, así:
¿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
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”!
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í:
- 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
es20
puntos - … pero también hacia afuera en todas las direcciones — una
shadowSpread
(extensión de la sombra) de10
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í:
- El álbum es 249 x 249 — lo que hace una
scale
de0.8
- La sombra es muy ligera: solo 10% negro —
"rgba(0,0,0,0.1)"
- Una
shadowY
de19
- Nada de
shadowSpread
- Una
shadowBlur
de37
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"
lleva0.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 deBezier.ease
), y la duración de esa animación es de0.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"
.)
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"
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 delprogressBar
. - 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.
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
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.
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.
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) …
… 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í:
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
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
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.
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"
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.
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: firstSetDurationscroll_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:
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:
Ú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:
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):
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
.
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: secondSetDurationscroll_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
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.
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()
.)
¡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!