[Windows 10] Experiencias multipantalla utilizando ProjectionManager

Introducción

Entre el conjunto de posibilidades nuevas disponibles con Windows 10, sin duda alguna, hay una brilla fuertemente sobre las demas, Continuum. Esta característica permite conectar un teléfono a un monitor externo permitiendo interactuar con la App en modo escritorio mientras podemos continuar utilizando el teléfono.

Continuum

Continuum

Es vital utilizar los nuevos AdaptiveTriggers, RelativePanel además de controlar el modo de interacción y otros detalles para adaptar la interfaz y usabilidad a cada posible situación. De esta forma conseguimos aplicaciones adaptativas pudiendo ofrecer la experiencia idónea en cada familia de dispositivo soportado.

En Continuum podemos tener una única App en la pantalla secundaria de forma simultánea. Sin embargo, podemos crear experiencias con múltiples pantallas. ¿Os imagináis ver listado de restaurantes cercanos en el teléfono mientras que en pantalla grande vemos mapa mostrando cercanía a nuestra posisión y críticas?, ¿ tener detalles de una película y ver el trailer de la misma en pantalla completa?. Escenarios donde sacar partido de la proyección de información a una pantalla secundaria hay muchos tanto en aplicaciones como en juegos. En este artículo vamos a sacarle todo el partido a la clase ProjectionManager y el trabajo multipantalla.

ProjectionManager

ProjectionManager

Proyección de vistas

Crearemos un nuevo proyecto UAP:

Nueva App UAP

Nueva App UAP

Añadimos las carpetas Views, ViewModels y Services además de las clases base necesarias para implementar el patrón MVVM de la misma forma que vimos en este artículo.

El objetivo del artículo será proyectar una pantalla secundaria para aprender a:

  • Proyectar pantalla secundaria.
  • Detener la proyección.
  • Hacer un intercambio de la pantalla donde se proyecta.

Detectar pantalla secundaria

ProjectionManager nos permite proyectar una ventana de nuestra App en una pantalla secundaria. A nivel de desarrollo, el proceso es similar a trabajar con múltiples ventanas en la misma App. Para proyectar en otra pantalla lo primero que debemos verificar es si disponemos de esa pantalla.

En nuestro ejemplo, mostraremos en la interfaz si contamos o no con la pantalla donde proyectar:

<TextBlock 
     Text="{Binding IsProjectionDisplayAvailable}"/>

Usaremos una sencilla propiedad bool en la viewmodel:

private bool _isProjectionDisplayAvailable;

public bool IsProjectionDisplayAvailable
{
     get { return _isProjectionDisplayAvailable; }
     set
     {
          _isProjectionDisplayAvailable = value;
          RaisePropertyChanged();
     }
}

En la clase ProjectionManager contamos con el evento ProjectionDisplayAvailableChanged que se lanza cada vez que la pantalla secundaria sobre la que proyecta pasa a estar disponible o no disponible:

ProjectionManager.ProjectionDisplayAvailableChanged += ProjectionManager_ProjectionDisplayAvailableChanged;

También podemos realizar la verificación de si tenemos disponible la pantalla secundaris utilizando la propiedad ProjectionDisplayAvailable:

IsProjectionDisplayAvailable = ProjectionManager.ProjectionDisplayAvailable;

NOTA: Si no contamos con pantalla secundaria, la vista proyectada se mostrará en la misma pantalla donde se encuentra la vista principal.

Proyectar

Conocemos como verificar si contamos con pantalla secundaria sobre la que proyectar, veamos como realizar la proyección.

En nuestra interfaz tendremos un botón que nos permitirá proyectar una vista específica:

<Button 
     Content="Project"
     Command="{Binding ProjectCommand}"/>

Nuestra interfaz principal:

Vista principal

Vista principal

El comando a ejecutar:

private ICommand _projectCommand;

public ICommand ProjectCommand
{
     get { return _projectCommand = _projectCommand ?? new DelegateCommandAsync(ProjectCommandExecute); }
}

public async Task ProjectCommandExecute()
{
     App.MainViewId = await _projectionService.ProjectAsync(typeof(ProjectionView));
}

Hemos creado un servicio ProjectionService en el que tenemos agrupada toda la lógica de proyección. Para proyectar utilizamos el siguiente método:

public async Task<int> ProjectAsync(Type viewType, DeviceInformation device = null)
{
     int mainViewId = ApplicationView.GetForCurrentView().Id;
     int? secondViewId = null;

     var view = CoreApplication.CreateNewView();
     await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
     {
          secondViewId = ApplicationView.GetForCurrentView().Id;
          var rootFrame = new Frame();
          rootFrame.Navigate(viewType, null);
          Window.Current.Content = rootFrame;
          Window.Current.Activate();
     });

     if (secondViewId.HasValue)
     {
          if(device == null)
              await ProjectionManager.StartProjectingAsync(secondViewId.Value, mainViewId);
          else
              await ProjectionManager.StartProjectingAsync(secondViewId.Value, mainViewId, device);
     }

     return mainViewId;
}

Para realizar la proyección utilizamos el método StartProjectingAsync(Int32,Int32) al que le pasamos como parámetros:

  • ProjectionViewId: El  identificador de la ventana que se va a mostrar en la pantalla secundaria.
  • AnchorViewId: El identificador de la ventana original.

Comenzamos creando una nueva vista vacía en blanca. En esta vista navegamos a la vista que deseamos proyectar y la asignamos como contenido. Podemos pasar los parámetros necesarios en este punto.

NOTA: Es totalmente necesario realizar la llamada a Window.Current.Activate para que la vista pueda visualizarse.

La vista no aparecerá hasta lanzar el método StartProjectingAsync. Tras lanzarlo, colocamos una vista existente en una pantalla secundaria, en caso de detectar una. De lo contrario, la vista se sitúa en el monitor principal.

Proyectar seleccionando la pantalla

Lo visto hasta este punto es sencillo y efectivo. Sin embargo, podemos tener situaciones más complejas con múltiples pantallas sencundarias.

¿Podemos elegir sobre que pantalla proyectar?

Si, podemos. Vamos a ver como realizar este proceso. Creamos en la interfaz otro botón de modo que al ser pulsado nos muestre todas las pantallas disponibles. Una vez seleccionada una pantalla específica proyectaríamos sobre la misma:

<Button 
     Content="Select Target and Project"
     Command="{Binding SelectTargetCommand}"/>
<ListView 
     ItemsSource="{Binding Devices}"
     SelectedItem="{Binding SelectedDevice, Mode=TwoWay}"
     Height="300"
     Width="300"
     HorizontalAlignment="Left">
     <ListView.ItemTemplate>
          <DataTemplate>
               <TextBlock Text="{Binding Name}" />
          </DataTemplate>
     </ListView.ItemTemplate>
</ListView>

En la viewmodel:

private ICommand _selectTargetCommand;

public ICommand SelectTargetCommand
{
     get { return _selectTargetCommand = _selectTargetCommand ?? new DelegateCommandAsync(SelectTargetCommandExecute); }
}

public async Task SelectTargetCommandExecute()
{
     try
     {
          Devices = new ObservableCollection<DeviceInformation>(await _projectionService.GetProjectionDevices());
     }
     catch (Exception ex)
     {
          Debug.WriteLine(ex.Message);
     }
}

Utilizamos el siguiente método:

public async Task<IEnumerable<DeviceInformation>> GetProjectionDevices()
{
     // List wired/wireless displays
     String projectorSelectorQuery = ProjectionManager.GetDeviceSelector();

     // Use device API to find devices based on the query 
     var projectionDevices = await DeviceInformation.FindAllAsync(projectorSelectorQuery);

     var devices = new ObservableCollection<DeviceInformation>();
     foreach (var device in projectionDevices)
          devices.Add(device);

     return devices;
}

Utilizamos el método GetDeviceSelector disponible en ProjectionManager que nos devuelve una cadena con la enumeración de dispositivos disponibles. Utilizamos la cadena para obtener una colección de dispositivos (DeviceInformation) en los cuales tenemos toda la información necesaria.

La colección obtenida es la que bindeamos a nuestra interfaz. Una vez seleccionado un dispositivo concreto:

private async Task Project(DeviceInformation device)
{
     try
     {
          // Show the view on a second display (if available) 
          App.MainViewId = await _projectionService.ProjectAsync(typeof(ProjectionView), device);

          Debug.WriteLine("Projection started in {0} successfully!", device.Name);
     }
     catch (Exception ex)
     {
          Debug.WriteLine(ex.Message);
     }
}

Utilizamos un método al que le pasamos el dispositivo y se encarga de realizar la proyección. En este caso, en nuestro servicio utilizamos el método StartProjectingAsync(Int32,Int32,DeviceInformation) donde además de los identificadores de la nueva vista y de la original, indicamos el dispositivo, es decir, la pantalla específica sobre la que proyectar.

También podemos de forma muy sencilla permitir elegir el dispositivo sobre el que proyectar utilizando el método RequestStartProjectingAsync(Int32,Int32,Rect,Placement). Utilizando este método se mostrará un flyout con el listado de dispositivos, de modo que, una vez seleccionado uno, comenzamos la proyección. Para indicar la posición del flyout podemos utilizar un parámetro de tipo Rect.

public async Task<int> RequestProjectAsync(Type viewType, Rect? position = null)
{
     int mainViewId = ApplicationView.GetForCurrentView().Id;
     int? secondViewId = null;

     var view = CoreApplication.CreateNewView();
     await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
     {
          secondViewId = ApplicationView.GetForCurrentView().Id;
          var rootFrame = new Frame();
          rootFrame.Navigate(viewType, mainViewId);
          Window.Current.Content = rootFrame;
          Window.Current.Activate();
     });

     if (secondViewId.HasValue)
     {
          var defaultPosition = new Rect(0.0, 0.0, 200.0, 200.0);
          await ProjectionManager.RequestStartProjectingAsync(secondViewId.Value, mainViewId, position.HasValue ? position.Value : defaultPosition);
     }

     return mainViewId;
}

La vista proyectada

¿Y que ocurre con la vista proyecta?. Nada en especial, puede ser cualquier vista de la aplicación. Sin embargo, puede llegar a interesarnos realizar algunas interacciones con la API de proyección como:

  • Detener la proyección.
  • Modificar el dispositivo donde proyectamos.

La interfaz de usuario contará con dos botones, uno para detener la proyección y otro para modificar el dispositivo utilizado.

<Grid>
     <StackPanel>
          <TextBlock
              Text="Projection View"
              FontWeight="SemiBold"/>
          <Button 
              Content="Swap"
              Command="{Binding SwitchViewCommand}"/>
          <Button
              Content="Stop"
              Command="{Binding StopCommand}"/>
     </StackPanel>
</Grid>

El resultado:

Vista proyectada

Vista proyectada

En la viewmodel:

private ICommand _switchViewCommand;
private ICommand _stopCommand;

public ICommand SwitchViewCommand
{
     get { return _switchViewCommand = _switchViewCommand ?? new DelegateCommandAsync(SwitchViewCommandExecute); }
}

public ICommand StopCommand
{
     get { return _stopCommand = _stopCommand ?? new DelegateCommandAsync(StopCommandExecute); }
}

public async Task SwitchViewCommandExecute()
{
     try
     {
          await _projectionService.SwitchProjection(App.MainViewId);
     }
     catch (Exception ex)
     {
          Debug.WriteLine(ex.Message);
     }
}

public async Task StopCommandExecute()
{
     try
     {
          await _projectionService.StopProjection(App.MainViewId);
     }
     catch (Exception ex)
     {
          Debug.WriteLine(ex.Message);
     }
}

Detener proyección

Para detener la proyección tenemos a nuestra disposición el método StopProjectingAsync que oculta la vista mostrada en proyector o pantalla secundaria.

public async Task StopProjection(int mainViewId)
{
     await ProjectionManager.StopProjectingAsync(       
                ApplicationView.GetForCurrentView().Id,
                mainViewId);
}

Cambiar pantalla

Podemos cambiar al vuelo la pantalla donde se realiza la proyección utilizando el método SwapDisplaysForViewsAsync de la clase ProjectionManager:

public async Task SwapProjection(int mainViewId)
{
     await ProjectionManager.SwapDisplaysForViewsAsync(
                ApplicationView.GetForCurrentView().Id,
                mainViewId);
}

Tenéis el código fuente disponible e GitHub:

Ver GitHub

Recordar que podéis dejar en los comentarios cualquier tipo de sugerencia o pregunta.

Más información

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s