25 June 2014

Building multi-resolution popups in an Universal App

With the advent of Windows Phone 8.1 multiple resolution are now also becoming a fact of life for Windows Phone developers (as they already were for Windows Store developers). This has a few side effects, which will require you to think about the size of things that need to cover part of the screen. Particularly you can get into trouble with animations built with visual states, which tend to have fixed number in them. I already dabbled into this when I ported Two Phone Pong to Windows 8.1 and had to make a screen-size independent scroll-into-view pane.

Now, for a demo app I have been working on, I wanted to make a popup that always used 1/3th of the screen. That used to be easy, as we had only one resolution, more or less, and for the rest Windows Phone 8 did some auto scaling. Now it needs a little more work. Very little, as it turned out I almost had all the pieces in place:

  • I already had the UnfoldBehavior, which can be used to let UI elements appear and disappear by ‘folding’ them in or out.
  • I already had the SizeListenerBehavior, which allows me to data bind to the actual width and height of an element

The only thing that was missing was this very simple converter:

using System;
using System.Globalization;

namespace WpWinNl.Converters
{
  public class PercentageConverter : BaseValueConverter
  {
    public override object Convert(object value, Type targetType, 
      object parameter, CultureInfo culture)
    {
      double parmValue;
      if (value is double && double.TryParse(parameter.ToString(), 
                                            out parmValue))
      {
        value = (double)value * parmValue / 100;
      }
      return value;
    }

    public override object ConvertBack(object value, Type targetType, 
      object parameter, CultureInfo culture)
    {
      if (value is double && (parameter is double || parameter is int))
      {
        value = (double)value / 100 / (double)parameter;
      }
      return value;
    }
  }
}

Basically all it does is dividing the incoming value by 100 and multiply it with the parameter value, so if you supply “33” as ConverterParameter is will multiply the incoming value by 0.33, effectively dividing it by 3 (ish).

So now I can define a simple app with a map (for instance), and when the popup is displayed, it always takes the bottom third of the screen. Regardless of screen resolution.

On the 4” WVGA (800x480) emulator this looks like this:

imageimage

While on a 1080p 6” emulator it looks like this:

imageimage

Added bonus: the popup resizes automatically as you rotate the phone. It always takes the bottom third of the screen.

The way to achieve this is pretty simple:

<!-- some stuff snipped -->
<Grid>
  <interactivity:Interaction.Behaviors>
    <behaviors:SizeListenerBehavior x:Name="ContentRootListener"/>
  </interactivity:Interaction.Behaviors>
  <!-- some stuff snipped -->
  
  <Grid Grid.Row="1" Margin="19,9.5,19,0">
    <maps:MapControl>
    </maps:MapControl>
    <!-- the Grid below is the actual popup window -->
    <Grid Height="{Binding WatchedObjectHeight, 
      ElementName=ContentRootListener, Converter={StaticResource PercentageConverter}, 
      ConverterParameter=33}" VerticalAlignment="Bottom" Background="Blue" >
      <interactivity:Interaction.Behaviors>
        <behaviors:UnfoldBehavior RenderTransformY="1" Direction="Vertical" 
                                  Activated="{Binding IsOpen}"/>
      </interactivity:Interaction.Behaviors>
      <StackPanel>
        <TextBlock Text="This is my popup" FontSize="22"></TextBlock>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="It's height is:" FontSize="22"></TextBlock>
          <TextBlock Text="{Binding WatchedObjectHeight, 
            ElementName=ContentRootListener, Converter={StaticResource PercentageConverter}, 
            ConverterParameter=33}" 
            FontSize="22"></TextBlock>
        </StackPanel>
      </StackPanel>
    </Grid>
  </Grid>
</Grid>
  • The top grid has a SizeListenerBehavior. This enables me to bind to the ActualHeight and of the top grid as it changes by binding to WatchedObjectHeight of the SizeListenerBehavior.
  • The height of the popup is set like by actually binding to said value. By using the converter with a parameter of 33 it will – when activated – always use 1/3rd of the screen – or 1/3rd of whatever element your SizeListenerBehavior is sitting on.
  • By setting the UnfoldBehavior’s RenderTransformY to 1 and the Direction to Vertical the popup will open from the bottom up. This is not new, that was already in the original article about UnfoldBehavior.

I have included a small demo solution to show off this little trick. It uses my pre-release version of WpWinNl 2.0.x and a little MVVMLight, but you could use this just as easy from a non-MVVM app. But I still think the beauty of this all is that can be initiated from a viewmodel ;)

In addition, although the demo solution only contains a Windows Phone app, this trick works just as well as on Windows Store apps, as both UnfoldBehavior and SizeListenerBehavior are already in the new WpWinNl NuGet package.

No comments: