13 December 2014

A behavior to deal with UI consequences of full screen and Software Keys in Windows Phone 8.1

This week I was presented with an interesting challenge. Using this technique, I used the whole screen for my app. The problem was I had not anticipated a Denim feature for the so called software buttons. For those unfamiliar with that – on the newest low and mid-tier phones the back, start and search buttons are not necessarily hardware buttons anymore, but can be a dedicated piece of screen that shows buttons. This enables hardware manufacturers to make phones for all platforms in one (hardware) package. Now the Denim firmware – that comes with the Lumia 73x and 83x - enables users to make those software buttons disappear – so the extra piece of screen can be used by the app itself. Pretty awesome. This can be done by using pressing a little arrow that appears on the left of the button bar:image


It can be brought up again by swiping in from the bottom of the imagescreen. Pretty cool, but with a side effect I had not anticipated. If the application view bound mode is set to ApplicationViewBoundsMode.UseCoreWindow in App.Xaml.cs the phone reports the whole screen size – not only the part that is normally taken by the status bar on top and the application bar at the bottom, but also the part that is used by the button bar. The My SensorCore app Travalyzer employs slide-in ‘windows’ that slide in from the side and stick to the bottom. I just took an offset the size of the application bar, and I was done, right? Yeah. Until my app got deployed to Denim phones. When the button bar is hidden, there is no problem as you can see to the right. But when it is not hidden… image

I believe the correct word in this circumstance is something like “blimey”, or maybe “crikey”, depending on what kind part of the globe you come from.

The solution is – you guessed it – a behavior. Or actually, two. But one is an oldie. Never one for original names, I have called it KeepFromBottomBehavior.

The code is actually surprisingly simple:
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;

namespace WpWinNl.Behaviors
{
  public class KeepFromBottomBehavior : SafeBehavior<FrameworkElement>
  {
    private double originalBottomMargin;
    protected override void OnSetup()
    {
      originalBottomMargin = AssociatedObject.Margin.Bottom;
      UpdateBottomMargin();
      ApplicationView.GetForCurrentView().VisibleBoundsChanged += 
        KeepInViewBehaviorVisibleBoundsChanged;

      base.OnSetup();
    }

    void KeepInViewBehaviorVisibleBoundsChanged(ApplicationView sender, object args)
    {
      UpdateBottomMargin();
    }

    private void UpdateBottomMargin()
    {
      if (WindowHeight > 0.01)
      {
        var currentMargins = AssociatedObject.Margin;

        var newMargin = new Thickness(
          currentMargins.Left, currentMargins.Top, currentMargins.Right,
          originalBottomMargin + 
            (WindowHeight - ApplicationView.GetForCurrentView().VisibleBounds.Bottom));
        AssociatedObject.Margin = newMargin;
      }
    }

    #region WindowHeight
  }
}

Like all my behaviors, it’s a SafeBehavior so you have a nice and easy base class. It first saves the current Bottom margin, and then calls the “UpdateBottomMargin” method. That method assumes “WindowHeight” contains the actual (full) height of the space available to the app. It subtracts that from that height the bottom of the rectangle depicting the visible bounds, that is – the part that is, according to the phone, not obscured by an App Bar. That it adds to the original bottom margin (in my app that is zero – I want the window to stick to the very bottom). Net effect: the object to which this behavior is attached always moves upward and downward if the user opens or closes the ‘software button bar’, and if he rotates the phone, it takes that into account as well.

Now WindowHeight is a region (yeah flame me, I use that for Dependency properties) containing a  Dependency property that calls UpdateBottomMargin as well if the WindowHeight changes.

public const string WindowHeightPropertyName = "WindowHeight";

public double WindowHeight
{
  get { return (double)GetValue(WindowHeightProperty); }
  set { SetValue(WindowHeightProperty, value); }
}

public static readonly DependencyProperty WindowHeightProperty = DependencyProperty.Register(
    WindowHeightPropertyName,
    typeof(double),
    typeof(KeepFromBottomBehavior),
    new PropertyMetadata(default(double), WindowHeightChanged));
 
public static void WindowHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  var thisobj = d as KeepFromBottomBehavior;
  var newValue = (double)e.NewValue;
  if (thisobj != null)
  {
    thisobj.UpdateBottomMargin();
  }
}

But how does this property get it’s value? Enter our old friend SizeListenerBehavior. How this all works, will be demonstrated using a simple app. First,we need to have an emulator capable of displaying software keys. The 8.1 U1 WXGA 4.5 inch fits the bill.To enable soft keys display, you will need to open the additional tools, and under sensor you will find the “Software buttons” checkbox. The emulator will reboot, and then show a screen with software keysimageimageEven with just hardware keys most of the ‘popup’ already disappears behind the app bar, but if you hide the software keys, then swipe them up again, it indeed looks pretty bad – the text “Some popup” has disappeared behind the software buttons bar, and most of the controls that could be there are hardly readable, let alone usable to the user.

The XAML for this page is not quite the most complex in the world.
<Page [name space stuff omitted]
    >
  <Page.BottomAppBar>
    <CommandBar Opacity="0.7">
      <AppBarButton Icon="Accept" Label="Click"/>
    </CommandBar>
  </Page.BottomAppBar>

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <!-- Title Panel -->
    <StackPanel Grid.Row="0" Margin="12,0,0,0">
      <TextBlock  Text="MY APP" Style="{ThemeResource TitleTextBlockStyle}" 
                  Margin="0,12,0,0" />
      <TextBlock Text="a map" Margin="0,-6.5,0,26.5" 
                 Style="{ThemeResource HeaderTextBlockStyle}" 
                 CharacterSpacing="{ThemeResource PivotHeaderItemCharacterSpacing}" 
                 VerticalAlignment="Top"/>
    </StackPanel>

    <Maps:MapControl Grid.Row="1"/>
    <Grid Height="150" Grid.Row="1" VerticalAlignment="Bottom" 
      Background="#FF7A2222" >
      <TextBlock Text="Some popup"  
                 Style="{ThemeResource TitleTextBlockStyle}" 
                 VerticalAlignment="Bottom" HorizontalAlignment="Center" />
    </Grid>
  </Grid>
</Page>

Now to solve the problem, follow just these easy steps:

  1. Pull in the newest version of WpWinNl from NuGet. Make sure you have set the NuGet package manager settings to “including prerelease’. You will need to have the 2.1.2 alpha version or higher. Don’t worry about that alpha – I am already using this in my apps, I just haven’t got around to making this a final version.
  2. Compile the application
  3. Open the Windows Phone project in Blend
  4. Find the SizeListenerBehavior in “Assets” , drag it on top of the Page Element, rename it to ContentRootListener
    image
  5. Find the “KeepFromBottomBehavior”, then drag it on top of the grid holding the ‘popup’
    image
  6. On the right hand side, find the “Properties” tab and select the little square beside “WindowHeight”
    image
    In the popup menu, select “Bind to Element”
  7. Now select ContentRootListener element again (in the Objects and TimeLine tab where you just put it in step 4
  8. Select WatchedObjectHeight. That’s it. You are done.
The XAML now looks like this:
<Page [name space stuff omitted]
   >
  <Page.BottomAppBar>
    <CommandBar Opacity="0.7">
      <AppBarButton Icon="Accept" Label="Click"/>
    </CommandBar>
  </Page.BottomAppBar>

  <Interactivity:Interaction.Behaviors>
    <Behaviors:SizeListenerBehavior x:Name="ContentRootListener"/>
  </Interactivity:Interaction.Behaviors>

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <!-- Title Panel -->
    <StackPanel Grid.Row="0" Margin="12,0,0,0">
      <TextBlock  Text="MY APP" Style="{ThemeResource TitleTextBlockStyle}" 
        Margin="0,12,0,0" />
      <TextBlock Text="a map" Margin="0,-6.5,0,26.5" 
        Style="{ThemeResource HeaderTextBlockStyle}" 
        CharacterSpacing="{ThemeResource PivotHeaderItemCharacterSpacing}" 
        VerticalAlignment="Top"/>
    </StackPanel>

    <Maps:MapControl Grid.Row="1"/>
    <Grid Height="150"  Grid.Row="1" VerticalAlignment="Bottom" 
    Background="#FF7A2222" >
      <Interactivity:Interaction.Behaviors>
        <Behaviors:KeepFromBottomBehavior 
        WindowHeight="{Binding WatchedObjectHeight, 
        ElementName=ContentRootListener}"/>
      </Interactivity:Interaction.Behaviors>
      <TextBlock Text="Some popup"  
        Style="{ThemeResource TitleTextBlockStyle}" 
        VerticalAlignment="Bottom" HorizontalAlignment="Center" />
    </Grid>
  </Grid>
</Page

In bold and red you see the new parts. And sure enough, if you now watch the ‘popup’ it will always be above the App Bar – it will even move dynamically move up and down if you open and close the software buttons.

imageimage

Problem solved, case closed. You don’t even have to type code because the behavior is already in WpWinNl.

Mind you – this behavior works only on Windows Phone 8.1, since it only is applicable to Windows Phone. That’s because the ApplicationView.GetForCurrentView().VisibleBoundsChanged event is only available on Windows Phone.

Demo solutions – before and after adding NuGet Package and behaviors – can be found here. Mind you – both solutions contain a Windows 8.1 project, but that does not do anything.

Special thanks to my colleague Valentijn Makkenze for first noticing this problem, Ben Greeve and this guy for sending me screenshots, and Hermit Dave for some valuable pointers.

No comments: