Visually Located

XAML and GIS

Keeping ads in the same location when the phone orientation changes to landscape

There has been a lot of information flying around about ads in apps these days. Microsoft recently updated PubCenter reporting to include fill rates and number of requests for ads. Dvlup recently partnered with AdDuplex to in its reward program. With all of this hype, I thought I would talk about a common problem with placing ads in apps. That issue is keeping ads in the same location when rotating the phone. Most apps are Portrait apps and display ads either at the top or bottom of the app.

image

Displaying ads like this can be done with the following xaml

<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <StackPanel Grid.Row="0" Margin="12,17,0,28">
        <TextBlock Text="STATIONARY AD" Style="{StaticResource PhoneTextNormalStyle}"/>
        <TextBlock Text="sample" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
    </StackPanel>
    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
        <!-- Other content here -->
    </Grid>
    <adDuplex:AdControl Grid.Row="2"/>
</Grid>

This style works really well for most apps, but what happens when you are displaying data that goes off of the screen? You could either say “Oh well”, you could wrap the text, or you could allow the user to rotate the phone so they can see more. Allowing the content of the page to display in Landscape is as simple as setting SupportedOrientations of the page to PortraitOrLandscape. Doing so allows the user to see more content, but has a side effect of the ad control taking up a lot of room at the bottom of the page.

image

When supporting Landscape, you really want the ads to stay stationary.

image

To solve this we’ll create a new Panel like control that will replace the “LayoutRoot” Grid of the standard page. I recently discovered that in two years of blogging I have yet to talk about creating a custom control. We’ll solve this problem by doing just that! A custom control is a control that is not derived from UserControl. Custom controls have a lot of benefits. My top two benefits are reusability and templating (ability to restyle the control). For this we’ll need to create a new ContentControl. A ContentControl allows for a single child to be placed within the xaml element or set from the Content property.

We’ll start by creating two files, one for code and one for xaml. Create a folder named StationaryAdPanel. In the folder add a new code file names StationaryAdPanel.cs and one code file named StationaryAdPanel.theme.xaml.

image

In the cs file, start defining the control.

public class StationaryAdPanel : ContentControl
{
    public StationaryAdPanel()
    {
        DefaultStyleKey = typeof(StationaryAdPanel);
    }
}

The constructor defines a DefaultStyleKey. This is the style that defines how the control looks. The style for the control needs to have a simple grid that will allow the ads to be moved to the sides that it needs to be on. Within the file add the following xaml that will define the layout of our control.

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Visual.Controls"
    xmlns:advertising="clr-namespace:Microsoft.Advertising.Mobile.UI;assembly=Microsoft.Advertising.Mobile.UI"
    xmlns:adDuplex="clr-namespace:AdDuplex;assembly=AdDuplex.WindowsPhone">
 
    <Style TargetType="local:StationaryAdPanel">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:StationaryAdPanel">
                    <Grid x:Name="PART_Root">
                        <Grid.RowDefinitions>
                            <!-- main content area -->
                            <RowDefinition Height="*"/>
                            <!-- ad area -->
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <!-- location for ads when rotated LandscapeRight -->
                            <ColumnDefinition Width="Auto"/>
                            <!-- Main content area -->
                            <ColumnDefinition Width="*"/>
                            <!-- location for ads when rotated LandscapeLeft -->
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        
                        <!-- main content of the page -->
                        <ContentPresenter  Grid.Column="1" Content="{TemplateBinding Content}"/>
                        
                        <!-- simple ad rotator, defaulted to bottom of page -->
                        <Grid x:Name="PART_StationaryPanel" Grid.Row="1" Grid.Column="1" 
                              RenderTransformOrigin=".5,.5" Width="480" Height="80">
                            <adDuplex:AdControl x:Name="AdDuplexControl" Visibility="Collapsed"/>
                            <advertising:AdControl x:Name="PubCenterControl" 
                                                   AdUnitId="Image480_80" ApplicationId="test_client"
                                                   Height="80" Width="480" 
                                                   IsAutoCollapseEnabled="True"/>
                        </Grid>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Next we’ll define the code for the control. The first step is to override the OnApplyTemplate. This is the method that we use to subscribe to any control events, or just get a control to use later. To keep the ads stationary, we’ll need to listen to when the Orientation of the page changes.

private PhoneApplicationPage _page;
private FrameworkElement _stationaryPanel;
private Grid _rootGrid;
private UIElement _adDuplexControl;
 
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
 
    _rootGrid = GetTemplateChild("PART_Root") as Grid ?? new Grid();
    _stationaryPanel = GetTemplateChild("PART_StationaryPanel") as FrameworkElement ?? new ContentPresenter();
    _stationaryPanel.RenderTransform = new RotateTransform();
    _adDuplexControl = (UIElement)GetTemplateChild("AdDuplexControl");
 
    var adControl = (AdControl)GetTemplateChild("PubCenterControl");
 
    // simple "ad rotator" methods to set visibility of ad controls
    adControl.ErrorOccurred += OnPubCenterErrorOccurred;
    adControl.AdRefreshed += OnPubCenterAdRefreshed;
 
    if (DesignerProperties.GetIsInDesignMode(this) == false)
    {
        // We will need to get the page we are in and listen for the
        // OrientationChanged event
        var frame = (Frame)Application.Current.RootVisual;
        _page = ((PhoneApplicationPage)frame.Content);
        if (_page.SupportedOrientations == SupportedPageOrientation.PortraitOrLandscape)
        {
            _page.OrientationChanged += OnOrientationChanged;
            OnOrientationChanged(_page, new OrientationChangedEventArgs(_page.Orientation));
        }
    }
}

The difficult part about keeping the ad stationary when in landscape is that we will need to rotate it. The downside to rotation is that other controls around the rotated control to not adjust for the rotation. This is solved with controls like the LayoutTransformer from Telerik or by porting the LayoutTransformer from the Silverlight toolkit. However, using these controls requires a dependency on a third party library that we may not want. To overcome this we will ne to adjust for the rotation ourselves. We’ll handle this within the OnOrientationChanged event handler.

private void OnOrientationChanged(object sender, OrientationChangedEventArgs args)
{
    // margin for shifting ad panel when in landscape mode
    // the margin is equal to (width - height)/2 = (480 - 80)/2 = 200
    const int margin = 200;
 
    // initial value of the margin of the page. If the SystemTray has an opacity
    // we need to shift the page content over
    Thickness pageMargin = _page.Margin;
 
    // margin use when rotating the ad
    Thickness rotateMargin = new Thickness(0, 0, 0, 0);
    
    // angle to rotate the ad (if in landscape)
    double angle = 0;
 
    int rotateRow = 0;
    int rotateRowspan = 0;
    int rotateColumn = 0;
 
    switch (args.Orientation)
    {
        case PageOrientation.None:
            break;
        case PageOrientation.Portrait:
        case PageOrientation.PortraitUp:
        case PageOrientation.PortraitDown:
            rotateRow = 2;
            rotateColumn = 1;
            rotateRowspan = 1;
            if ((SystemTray.Opacity > 0) && (SystemTray.Opacity < 1))
            {
                pageMargin = new Thickness(0, 32, 0, 0);
            }
            break;
        case PageOrientation.Landscape:
        case PageOrientation.LandscapeRight:
            rotateRowspan = _rootGrid.RowDefinitions.Count;
            rotateMargin = new Thickness(-margin, 0, -margin, 0);
            angle = 90;
            if ((SystemTray.Opacity > 0) && (SystemTray.Opacity < 1))
            {
                pageMargin = new Thickness(0, 0, 72, 0);
            }
            break;
        case PageOrientation.LandscapeLeft:
            rotateRowspan = _rootGrid.RowDefinitions.Count;
            rotateColumn = 2;
            rotateMargin = new Thickness(-margin, 0, -margin, 0);
            angle = -90;
            if ((SystemTray.Opacity > 0) && (SystemTray.Opacity < 1))
            {
                pageMargin = new Thickness(72, 0, 0, 0);
            }
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }
    _page.Margin = pageMargin;
 
    // now set location and rotation of ad panel
    _stationaryPanel.Margin = rotateMargin;
    ((RotateTransform)_stationaryPanel.RenderTransform).Angle = angle;
    Grid.SetRow(_stationaryPanel, rotateRow);
    Grid.SetColumn(_stationaryPanel, rotateColumn);
    Grid.SetRowSpan(_stationaryPanel, rotateRowspan);
}

We’ll wrap up the control by adding the methods to rotate the ads as needed.

private void OnPubCenterErrorOccurred(object sender, AdErrorEventArgs e)
{
    var pubCenterAd = ((UIElement)sender);
    if (pubCenterAd.Visibility == Visibility.Visible)
    {
        pubCenterAd.Visibility = Visibility.Collapsed;
        _adDuplexControl.Visibility = Visibility.Visible;
    }
}
 
private void OnPubCenterAdRefreshed(object sender, EventArgs eventArgs)
{
    var pubCenterAd = ((UIElement)sender);
    if (pubCenterAd.Visibility == Visibility.Collapsed)
    {
        pubCenterAd.Visibility = Visibility.Visible;
        _adDuplexControl.Visibility = Visibility.Collapsed;
    }
}

When writing custom controls, you need to define a “Generic.xaml” file located in a Themes folder in the root of the project. This is a requirement for custom controls. Nice file can contain the xaml style directly (the template we defined above) or it can reference the xaml files used to define the templates. I prefer to put the styles and templates in separate files rather than flood the Generic.xaml file. To start, create a folder named “Themes” in the root of the project. Right click the folder and add a new code file named “Generic.xaml”. In the file paste the following code (changing YOUR_PROJECT_NAME with the name of the project).

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/YOUR_PROJECT_NAME;component/Controls/StationaryAdPanel/StationaryAdPanel.Theme.xaml" />
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

We are now ready to use the control in our pages. The control is really simple to use. Take the LayoutRoot grid that you normally have, and wrap it with the new StationaryAdPanel.

<local:StationaryAdPanel>
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <!-- regular content -->
    </Grid>
</local:StationaryAdPanel>

With the root grid wrapped, we can rotate our apps to see more text.

image                     image

You can download a complete working sample from here.

blog comments powered by Disqus