El mundo de las apps en TV es todavía territorio inexplorado, ahora mismo hay apenas aplicaciones, aprovecha la oportunidad de realizar tu propia aplicación y darte a conocer.
Este taller te permitirá conocer los conceptos básicos y dar los primeros pasos en ambas plataformas: tvOS & AndroidTV, además nos permitirá comparar las bondades y peculiaridades de cada una de ellas.
1. TV Future is Apps
tvOS vs AndroidTV
by @pablito_az
@hugojperal
@jr_salazares
MADRID · NOV 18-19 · 2016
2. Agenda
¿Quiénes somos?
Introducción a la plataforma
Dinámica del workshop.
Primeros pasos
¿Cómo buscar contenido dentro de la app?
Vayamos al detalle.
Cosas que nos dejamos en el tintero
11. Para que sea compatible en AndroidTV ...
… tenemos que hacer algunas cosas:
● Nuestras activity principal CATEGORY_LEANBACK_LAUNCHER
● <uses-feature android:name = “android.software.leanback”
android:required = “false”>
● <uses-feature android:name = “android.hardware.touchscreen”
android:required = “false”>
12. … y nuestro dispositivo tiene limitaciones:
● Touchscreen
● Telephony
● Camera
● GPS
● Microphone
● Sensors
13. Adaptando el comportamiento ...
… en tiempo de ejecución:
if (getPackageManager().hasSystemFeature("android.hardware.camera")) {
// Hago algo con la cámara
} else {
// Adapto mi comportamiento
}
14. Algunas características de los layouts
Debemos tener en cuenta:
● Layouts en Landscape.
● UI en secciones fácilmente navegables.
● Suficiente margen entre elementos.
● Cuidado con el Overscan
● Textos suficientemente visibles.
15. v17 Leanback to the rescue
compile 'com.android.support:leanback-v17:24.2.1'
17. Configurando el BrowseFragment
Podemos configurar elementos en la interfaz muy fácil:
setBadgeDrawable(...);
setOnSearchClickedListener(...);
setSearchAffordanceColor(...);
19. Pintando contenido
class ContentAdapter extends ArrayObjectAdapter {
for (CategoryList) {
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new
CardItemPresenter());
for (awesomePlaces) {
listRowAdapter.add(places);
}
HeaderItem header = new HeaderItem(Category);
add(new ListRow(header, listRowAdapter));
}
}
20. Nuestro CardItemPresenter
public class CardItemPresenter extends Presenter{
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
// Creo mi ImageCardView aquí
}
@Override
public void onBindViewHolder(ViewHolder viewHolder,
Object item) {
// Pinto la información en la ImageCardView
}
}
21. Configurando ImageCardView
Tiene 3 elementos principales, además de la imágen de fondo:
Se pueden configurar, mediante un Style combinando estos
elementos:
<item name="lbImageCardViewType">Title|Content|IconOnLeft</item>
● Título
● Contenido
● Badge
24. Universal Purchase
Misma entrada en iTunes Connect
Mismo bundle ID
Subidas a iTunes Connect separadas
Distintas versiones
Distintos procesos de revisión
Esta unión es para… siempre
29. Templates, templates everywhere
18 templates a elegir
Elementos simples
Elementos compuestos
carousel, collectionList, shelf,
lockup
TVML
30. JS Principales
application.js:
JS Inicial. Carga otros JS y la primera pantalla
DocumentLoader:
Encargado de cargar documentos
DocumentController:
Es el controller y tiene asociado un loader y un
template. Nos abstrae de la navegación
TVML
31. Aspecto template
● Para navegar entre pantallas
usaremos el atributo
“documentURL” en cualquier
elemento focusable
TVML
35. SearchFragment
class SearchViewImp extends SearchFragment implements SearchResultProvider {
@Override
public ObjectAdapter getResultsAdapter() {
}
@Override
public boolean onQueryTextChange(String newQuery) {
}
@Override
public boolean onQueryTextSubmit(String query) {
}
}
36. Cards interaction: BackgroundManager
private BackgroundManager mBackgroundManager;
// Inicializamos el BackgroundManager
mBackgroundManager = BackgroundManager.getInstance(getActivity());
mBackgroundManager.attach(getActivity().getWindow());
// Cambiamos el fondo cada vez que seleccione una card
mBackgroundManager.setDrawable(backgroundDrawable);
38. ¿Cómo buscar dentro de la app?
//Swift
let searchResultsController = SearchResultsViewController()
let searchController = UISearchController(searchResultsController: searchResultsController)
searchController.searchResultsUpdater = searchResultsController
let searchContainer = UISearchContainerViewController(searchController: searchController)
49. Manejando recursos pesados
● Carga imágenes solo cuando se muestren en la pantalla.
● Recicla los Bitmaps cuando ya no sean necesarios.
● Si traes imágenes de red, hazlo siempre en un hilo aparte.
● Escala las imágenes al tamaño que realmente necesitas.
51. Cargar un video
Lo más fácil es usar la clase AVPlayerViewController y el
resto sería así:
//Creamos el player con la URL del video
player = AVPlayer(url: url)
//configuramos el payer con las opciones deseadas
player?.play()
54. TVML. Elementos Custom II
1.Implementar protocolo TVInterfaceCreating
2.Asignar el extendedInterfaceCreator del objeto
singleton TVInterfaceFactory
3.Registrar elemento
4.Usar en nuestros TVMLs
TVML
58. Finding content outside the app
● Recommendation Row:
○ Continuation content
○ New content
○ Related content
● Hacer que el contenido de tu aplicación sea encontrable
desde fuera de ella.
61. Cosas que nos dejamos en el tintero
Testing. Unit Test y UI Test
Top Shelf Dinámico
Parallax Artwork
Más Focus Engine:
∘ UIFocusGuide
∘ var preferredFocusEnvironments: [UIFocusEnvironment]
62. Cosas que nos dejamos en el tintero
Video features
Now playing
Animar DOM updates
63. Algunos enlaces
Primero pasos con AndroidTV
Building AndroidTV Apps
Guías de estilo AndroidTV
Distribución en AndroidTV
Como suele ser habitual en estos eventos, toca hacer un poco de publicidad de la empresa.
Los tres trabajamos en eDreams_Odigeo en el equipo de nativo. Jose y Hugo en la parte de iOS y yo en la parte de Android. No se si a todos os sonará la empresa, pero la actividad principal es la venta de vuelos. Además de eDreams, la empresa tiene todas las marcas que véis ahí: Opodo, GoVoyage, Liligo y Travelink.
En españa tenemos sede en Barcelona y Madrid, y aunque la empresa es española tiene sedes en otros sitios fuera de españa como Londres, Paris o Budapest.
Bueno y estamos contratando tanto en Madrid como Barcelona y podéis mirar todas las posciones que hay abiertas ahora mismo en el enlace que ponemos ahí. Más concretamente en el equipo de nativo también estamos contratando, tanto en Madrid como en Barcelona y si estáis interesados podéis aplicar directamente en el enlace, o en linkedin o directamente podéis escribirnos a cualquiera de los 3.
La experiencia en el salón y no en la palma de la mano. Entorno.
Las interacciones se hacen con el mando. Layout.
El mando de AppleTV es táctil, tiene micrófono y giroscópio. No obstante no podemos suponer este método de entrada. Otros dispositivos de entrada
La navegación tiene que ser clara e intuitiva. Con textos legibles a cierta distancia.
En todo momento tenemos que saber que elemento tiene el foco.
Se debe proporcionar una experiencia inmersiva y rica en contenido.
Antes de empezar, cuántos Androides hay? y iOS Developers? y cuántos saben JS?
Nuestro objetivo es que saquéis el máximo provecho de este workshop, si queréis que entremos en más detalle de algo tan sólo nos lo tenéis que decir.
Dicho esto, la dinámica que vamos a seguir (a priori) va a ser la siguiente: cada uno de nosotros vamos a hacer la misma app en tres tecnologías: mi compañero Pablo explicará AndroidTV, Hugo tvOS en Swift y yo haré lo propio con TVML.
Hemos dividido el proyecto en tres pasos incrementales, cada uno de ellos consta de una explicación seguida de unos breves ejercicios que podéis realizar en el tiempo que os daremos y en el cual podréis preguntarnos y consultar cualquier cosa que queráis. Los ejercicios están descritos en unos ficheros txt dentro del proyecto.
Iremos intercalando las tres tecnologías así que cuando no estemos presentando será el tiempo disponible para estos ejercicios y dudas.
Como he dicho, los pasos son incrementales, por lo que si os perdéis siempre podéis abrir el proyecto en el siguiente step.
Android tiene su repositorio y tvOS Swift y TVML comparten el mismo repositorio y proyecto pero diferentes targets. ¿Tenéis todos el material necesario?
Os parece bien? Pues venga. Comencemos
Si queremos que nuestra aplicación sea reconocida a través de Google Play, nuestra activity principal tiene que especificar que vamos utilizar la interfaz leanback. Para ello hay que categorizarla como CATEGORY_LEANBACK_LAUNCHER
De esta manera especificamos dos cosas: por un lado que nuestra aplicación utilizará la interfaz Leanback y por otro lado que no es obligatorio, es decir que si el dispositivo es compatible con esta interfaz la utilizará y si no, no. Si lo pusieramos a true (valor por defecto) nuestra aplicación sólo sería compatible con teles.
De la misma manera que el anterior tenemos que especificar el touchscreen como opcional, sino lo hacemos, la aplicación no será compatible con teles.
Si estamos adaptando una aplicación para que sea compatible con la TV, debemos marcar como opcionales todas estas funcionalidades.
Se recomienda tener distintos layouts y por lo tanto distintos comportamientos para una aplicación Móvil vs TV, no obstante si aún así decidimos adaptar nuestros layouts o compartimos controladores, podemos adaptar el comportamiento en tiempo de ejecución comprobando si el dispositivo es compatible con la funcionalidad que necesitamos.
Android TV apps are generally structured according to the following four modalities:
Browse View: The Browse View is the usual entry point to an Android TV application. Make it easy for users to navigate your content matrix by using relevant categories to simplify decision-making.
Detail View: The Detail View provides in-depth and relevant information for selected content. Place actionable content and the most relevant information in the immediately visible section of the screen, without requiring the user to scroll.
Consumption View: The Consumption View is for the user to engage with or watch content.
In-app Search: The In-app Search overlay provides a quick way to search for content within your app.
Las interfaces de la app se presentan en orientación horizontal. (Mandatory para play store)
Si dividimos la UI en secciones se facilita la navegación en horizontal y vertical mediante un mando.
Si asignamos el suficiente margen entre los elementos puede distinguirse de manera más sencilla que elemento de la interfaz es el que tiene el foco.
La app no exhibe texto ni funcionalidades que estén parcialmente cortados por los bordes de la pantalla. (Mandatory para playstore).
La app exhibe el texto central en un tamaño de 16 sp o más grande y La app exhibe el resto del texto en un tamaño de 12 sp o más grande. (Mandatory para playstore). Evitar utilizar letras ligeras o con trazos estrechos.
La librería de compatibilidad Leanback nos da algunos fragments muy útiles que podemos extender y configurar. Reutilizando las vistas que nos aporta la librería, no tenemos que preocuparnos de cumplir con las carácterísticas que hemos mencionado arriba ya que cumplen con todas las especificaciones necesarias ya que nos permite utilizar el Theme.Leanback en nuestras activities.
BrowseFragment nos permite agrupar contenido por categorías ya que está compuesto de RowFragment y HeaderFragment (que veremos en el siguiente paso).
Tiene métodos que nos permiten configurar muy fácil los elementos de esta vista.
Para pintar contenido dentro de nuestro BrowseFragment, nos vamos apoyar en otro elemento que nos ofrece la librería v17 Leanback: el ArrayObjectAdapter que estará compuesto por los objetos que queremos pintar (las cards en este caso) agrupados en una serie de headers items que hacen la función de categorías. Nuestro BrowseFragment se encargará de agruparlos y mostrarlos sin que tengamos que hacer prácticamente nada.
El comportamiento es muy similar a pintar elementos utilizando un RecyclerView por ejemplo. Tenemos nuestro adapter (ArrayContentAdapter) y tenemos nuestro ViewHolder (el elemento que nos proporciona la libreria de Leanback para esto lo llama Presenter). Como vemos aquí, solo tenemos que recorrer nuestra lista de categorías e ir creando un ArrayObjectAdapter de tipo RowAdapter (hay más tipos como SectionRow para crear solo secciones) para cada una incluyendo un CardPresenter (que veremos más adelante) por cada destino, y agrupándolos bajo un HeaderItem (categoría).
Como vemos, el Presenter hace las funciones de ViewHolder. Se utiliza para generar vistas y bindear contenido en estas vistas bajo demanda. Por cada elemento que añadimos a nuestro ArrayObjectAdapter anterior (destino) vamos a crear un ImageCardView.
El ImageCardView es otro de los elementos que nos ofrece la librería Leanback y nos permite montar de manera automática el elemento de las tarjetas así como la interacción que tengamos con esta.
Los elementos principales son 3: título, contenido (una pequeña descripción) y el badget que es un icono que se puede mostrar en el lado izquierdo o derecho de la tarjeta.
Android:
BrowseView tenemos que implementar el método paintAwesomePlaces
En el CardItemPresenter tenemos que configurar el tipo de card que queremos (buildDescriptionImageCardView o buildImageOnlyCardView). La que tiene descripción tiene que actualizar el background cuando tiene el foco
Limitación 200MB.
Se puede usar iCloud o CloudKit, NSUserDefaults, resources on demand. NSCachesDirectory NSTemporaryDirectory se pueden escribir. Solo 500KB?
#if os(tvOS)?
En el mismo proyecto puedes tener un target para la app de ios y otro para la de tvos. A parte puedes compartir código por frameworks.
Cuando empiece el step se puede explicar como se estructura el proyecto
En este paso añadiremos el botón izquierdo al navigation bar. Nos damos cuenta que el botón sigue las guías de diseño de Apple. Y que los elementos cuando tienen el foco suelen hacerse más grandes.
Lo más importante de un grid es saber quien tiene el foco. Saber si una vista puede tener el foco es importante, incluso si lo tiene. Pero también nos ayuda el didUpdateFocusInContext(_:withAnimationCoordinator:) para participar en la actualización del foco y en las animaciones asociadas. También podemos pedir una actualización del foco con setNeedsFocusUpdate() y updateFocusIfNeeded()
TVML es un formato especial de XML con etiquetas específicas definicas por Apple. Con este XML podemos definir las vistas
TVJS es Javascript con alguna peculiaridades específicas del AppleTV. Este será nuestro controllador. Desde la versión 10 se puede usar el estándar ECMAScript 6.
Por último TVMLKit es el puente entre la parte servidor y la app en sí.
¿Qué pinta tienen las apps hechas en TVML?
Por una parte tendremos la app instalada en el Apple TV que se comunicará con el servidor que aloja los ficheros JS y TVML. Normalmente los ficheros javascript serán los encargados de cargar las plantillas TVMLs, modificarlas y hacer la navegación.
¿Por qué usar TVML? Existen varias razones:
Todas las apps del AppStore realizadas por Apple están hechas en TVML.
Con muy poco esfuerzo podemos tener una app vistosa y acorde a la guía de estilos de Apple.
El resultado es una app nativa, no es un webview. TVMLKit se encarga de traducir los elementos TVML a elementos nativos.
Podemos actualizar la app sin tener que crear una nueva versión y al tener todos los elementos en el servidor, nuestra app ocupará muy poco
Por último, en el caso de que lo necesitemos, podemos invocar código Swift desde JS (por ejemplo, para usar un sdk de analytics), y también a la inversa, desde Swift a JS.
Dejar claro que la parte servidor no tiene nada que ver con el proyecto
Como ya somos expertos en TVML vamos a modificar el template que tenemos parael listado.Vamos a añadir una cabecera que contenga lo siguiente:* A la izquierda un botón con el icono de la lupa ("search.png")* En el centro un texto* A la derecha una imagen, que será nuestro logo ("logo.png")
Al igual que con el BrowseFragment, la librería de Leanback también nos facilita la vida a la hora de buscar contenido dentro de nuestra aplicación con otro Fragment nativo: searchFragment.
Para poder utilizarlo, solamente tenemos que crear un Fragment que extienda de SearchFragment e implementar SearchResultProvider que nos ayudará a la hora de pintar los resultados. El adapter es exactamente el mismo que el que utilizábamos en el BrowseView y los resultados se pintarán exactamente igual. Si no queremos que la búsqueda se lance cada vez que el usuario introduce un nuevo carácter, es conveniente retrasarla algunos milisegundos (300 sueles ser suficiente).
Vamos a interactuar con las tarjetas cambiando el fondo de la pantalla cada vez que una tarjeta es seleccionada, tanto el Browse como en el Search. Para eso vamos a utilizar el BackgroundManager que nos permite mantener la misma imágen de Background entre distintas activites. Al igual que las search, no queremos que se actualice si el usuario pasa muy rápido por las tarjetas, por lo que vamos a meterle un retardo también de 300ms
Android:
Para la parte de la búsqueda, implementar los métodos getResultAdapter y doSearch. No es necesario pintar de nuevo las cards porque es exactamente igual en el paso anterior
Para la parte del background: implementar setUpUIElements, prepareBackgroundManager y updateBackground tanto en SearchView como en BrowseView
El result controller normalmente tiene un grid que se encarga de mostrar los resultados.
Se crea el search controller. Las actualizaciones del search las suele manejar el result.
Ponemos el search controller en el container y lo presentamos.
Podemos crearnos distintos gestures recognizers. Para dar compatibilidad a distintos mandos o cambiar comportamiento a los botones como el menu, imaginaros en un juego darle a menu y salirnos de la partida.
target es quien disparó el evento. currentTarget es quien tenía asignado el evento.
modificar el DOM. Depurar
En este paso programaremos un poco de JS. El objetivo de este step es setear el título del header del List.xml con eltítulo del elemento que tenga el foco. Para ello escucharemos al evento"highlight" del template.También queremos que cuando el usuario pulse el botón de play estando en ellistado, le aparezca la pantalla de búsqueda. El evento en ese caso es..."play" :)
Hasta ahora, nos hemos basados en componentes heredados de la librería de compatibilidad Leanback pero, ¿qué pasa si queremos construir nuestra propia vista? A través de la pantalla de detalle, veremos cómo es posible construir cualquier tipo de layout y cómo el framework nos puede facilitar la navegación a través de los distintos elementos. Para mi es el aspecto más importante: como hemos repetido durante todo el workshop, una navegación clara entre los distintos elementos.
Hay dos formas principales de controlar la navegación a través de los distintos elementos de la pantalla. La primera es esta: podemos setear el OnKeyListener y hacer lo que nosotros queramos dependiendo de la dirección hacia la que el usuario quiera navegar.
Como forma alternativa a lo anterior, el framework de Android ya provee un sistema de navegación direccional entre los elementos basándose en la posición relativa de los elementos focusables que además se puede modificar tal y como se ve en la transparencia: disponemos de nextFocusDown, nextFocusUp, nextFocusRight y nextFocusLeft.
Otro de los problemas importantes a los que nos tendremos que enfrentar es que puesto que la experiencia ha de ser lo más cinematográfica y envolvente posible, los más probable es que tengamos que manejar videos e imágenes en altas resoluciones en un dispositivo con un sistema limitado de memoria. Para evitar out of memory errors, debemos tener en cuenta algunas consideraciones:
Android:
Debemos de setear el onKeyListener para que llame al presenter
El presenter tiene que decidir que acción ejecutar dependiendo de la tecla que hayamos pulsado.
En este step cargaremos el controlador de detalle, pondremos el video y veremos una breve descripción del viaje.
Ejercicio: Poder usar de la lupa como resource
Para finalizar nuestro querido workshop vamos a hacer tres pequeñas tareas.1 -. En el listado, en vez de usar la imagen de la lupa descargada de internet, queremos usar la que tenemos dentro del proyecto. Para ello tendrás que implementar uno de los métodos del protocolo TVInterfaceCreating.
Swift → JS
Podemos obtener el contexto JS llamando al método evaluateInJavaScriptContext de la instancia TVApplicationController que se crea en el AppDelegate. Una vez tenemos el contexto lo único que tenemos que hacer es pasarle un evento mediante el método invokeMethod
2 -. Queremos enviar un evento desde Swift a TVJS, concretamente cuando detectemos que la app viene de background a foreground (applicationWillEnterForeground...)Para enviar el mensaje usaremos el método "executeRemoteMethod" que está en el AppDelegate. El javascript que reciba el evento, mostrará una alerta con un mensaje.
JS → Swift
3 -. Para ello tenemos que crear un protocolo en objc que implemente el protocol JSExport, ahí dentro pondremos el método que queramos (le podemos poner parámetros). A continuación crearemos una clase que extienda de NSObject y que implemente el protocolo que creamos antes. Lo último que nos queda es crear una instancia de nuestro objeto y setearlo en el contexto de JS, para ello necesitaremos implementar el método appController(appController, evaluateAppJavaScriptInjsContext) Haciendo esto tendremos siempre disponible en el JS la instancia del objeto bajo el nombre que le hemos dado en el último método. Lo que queda es llamar al método cuando queramos, por ejemplo cuando cambiamos el foco en el listado.BONUS-. Crea tu propio template! Tomando como referencia el template que hemos creado, haz lo propio pero esta vez para la búsqueda. No olvides registrar la vista en el TVElementFactory!
Content recommendations appear as the first row of the TV home screen after the first use of the device. Contributing recommendations from your app's content catalog can help bring users back to your app. Las recomendaciones se crean mediante procesos en background que de vez en cuando añaden el catálogo de tu aplicación al sistema de recomendaciones.
Embeber vídeos directamente en el TVML que se ejecutan cuando tienen el foco.