Fixing Pivot Control Layout Issues

PivotOpticalMarginFix

One of my first posts outlined the many discrepancies between the shipped project and page templates and the WP8.1 OS. While those most of the issues have been addressed, we now need to dig deeper and look at issues on the control level. The control that I will be tackling today will be the WinRT Pivot control. Even though there are many small issues that plague this control, they are almost all fixable by simple XAML retemplating.

The Material

VS_IconSAMPLE SOLUTION – see it in action
Code_IconRESOURCES – styles to copy+paste 

Pivot Header Issues

PivotOverlay

One of easiest issue to fix with the Pivot control is also the one of the more subtle ones. For some reason, unknown to me, the PivotItem headers do not use the normal default character spacing of zero. It’s actually more compressed than any other text used in Windows Phone. I’ve incorporated this fix in a previous post about issues with the shipped project templates but it’s worth reiterating here. Ideally, PivotItem’s header should look like Page headers since they follow the same layout structure (Basic pages are visually equivalent to Pivots with only one PivotItem). Because of this, there’s no real reason PivotItem headers should have a negative character spacing. To fix this, we simply just add a resource that will override the key “PivotHeaderItemCharacterSpacing.”

<x:Int32 x:Key="PivotHeaderItemCharacterSpacing">0</x:Int32>

Now, with this in your App.xaml, all PivotItem headers will automatically use our defined character spacing. Easy.

PivotOpticalMarginIss

The next issue with the headers is that it doesn’t have OpticalMarginAlignment because content presenters do not have this property. Because of this, any text being presented will have the default OpticalMarginAlignment of None. We do not want this. We want it to have an OpticalMarginAlignment of TrimSideBearings. To achieve this, we have to create a default style for the Pivot controls to have a header template that has this OpticalMarginAlignment. We do this like so:

<Style TargetType="Pivot">
    <Setter Property="HeaderTemplate">
        <Setter.Value>
            <DataTemplate>
                <TextBlock Text="{Binding}"
                           OpticalMarginAlignment="TrimSideBearings" />
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

Of course this will only work if you’re using an all text header. When using a more complex header, this template will need to be changed or removed accordingly. Now the last thing we need to do to make this perfectly match up with the native Pivot control headers is to adjust the margins to match. We can do this by adding override resources like we did earlier for the character spacing.

<x:Int32 x:Key="PivotHeaderItemCharacterSpacing">0</x:Int32>
<Thickness x:Key="PivotHeaderItemMargin">19,-6.5,5,0</Thickness>

PivotOpticalMarginFix

Annoying StatusBar Compensation

The Pivot is different than most, if not all, of the other controls in that it automatically compensates for the StatusBar at the top. By doing this, it tries to ensure that it has the same spacing from its Pivot title to the physical top of the screen regardless of whether a StatusBar is shown or not. This works in general but it causes some visual issues and unnecessary headaches in some situations. Because the page already handles this in most cases, the Pivot control is doing extra work and making itself behave inconsistently with other controls. This means we have to work harder and treat Pivots like a unique case rather than treating it like other controls.

Let’s attempt to understand a scenario where the Pivot is causing layout issues with its compensating. This can be seen easily using the SemanticZoom control. When the ZoomedOutView is displayed, a few things are actually happening to give this experience. A popup containing the ZoomedOutView is being shown while occluding UI elements are being hidden to give the popup a full screen view. One of those UI elements being hidden is the StatusBar. Since the StatusBar is now hidden, any Pivots on the page will try to pad the top of itself to compensate any vertical shifting that might occur. Unfortunately, the page doesn’t update visually appropriately when popups are open so the Pivot controls actually doesn’t need to do this. This causes Pivots to shift down and having extra padding on top.

PivotShiftingWithPopups

I’ve tried solutions through margin adjustments triggered by layout events but the result is usually inconsistent and not seamless enough. I finally came up with an acceptable solution after looking at the Pivot’s default control template and checking for margin changes through XAML Spy. When looking at the root Grid (named RootElement) in the Pivot’s default control template, we can see that its margin would change whenever the StatusBar was hidden or shown. From my understanding of templated controls, there are two ways to modify certain properties like margins of a control in a template: TemplateBinding or through reference by name. Since the root Grid didn’t have binding for its Margin property, I knew the Pivot control was looking for the Grid by name in code and then modifying its margin. To prevent this, I simply made an exact copy of the Pivot’s default control template in my App.xaml and omitted the x:Name of the root Grid. Since the Pivot control can’t find this element by name to modify its margin, the whole StatusBar compensation through margin manipulation shenanigan is no more.

<Style TargetType="Pivot">
    ...
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Pivot">
                <Grid HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                      VerticalAlignment="{TemplateBinding VerticalAlignment}"
                      Background="{TemplateBinding Background}">
                    ...

While I’m here modifying the template, I took the liberty of modifying some other values to better line it up with the native Pivot layout by adding a few additional override keys:

<Thickness x:Key="PivotPortraitThemePadding">19,12,0,0</Thickness>
<Thickness x:Key="PivotLandscapeThemePadding">19,18,0,0</Thickness>
<x:Int32 x:Key="PivotHeaderItemCharacterSpacing">0</x:Int32>
<Thickness x:Key="PivotHeaderItemMargin">19,-6.5,5,0</Thickness>

PivotShifting

Improper Pivot Item Margins

The last fix we need to apply has to do with the content or body of the Pivot itself. Each PivotItem in a Pivot control will generally present some content. By default, the content is spaced a certain distance from the edge of the display and the PivotItem headers above it. The spacing at the top is wrong so we’ll need to adjust it to the right value. That’s simple enough since it can be done the same we’ve been adjusting other values through resource overrides. The left and right margins, though, are technically correct. Content should be generally spaced from the screen edges by 19px. But the issue is I don’t like the control doing it automatically and unconditionally for me. When I want to put a ListView in a pivot, I want the right margin to be zero, not 19px, so the scrollbar can be flushed with the right edge of the screen. Also for consistency sake, basic pages don’t have left and right margins. We have to put them in ourselves. So with that reasoning, I’m adjusting the top AND the side margins like so:

<Thickness x:Key="PivotPortraitThemePadding">19,12,0,0</Thickness>
<Thickness x:Key="PivotLandscapeThemePadding">19,18,0,0</Thickness>
<x:Int32 x:Key="PivotHeaderItemCharacterSpacing">0</x:Int32>
<Thickness x:Key="PivotItemMargin">0,16,0,0</Thickness>
<Thickness x:Key="PivotHeaderItemMargin">19,-6.5,5,0</Thickness>

Conclusion

So to wrap up, we fixed PivotItem header’s character spacing, margins, optical margin alignment as well as PivotItem’s content margin. In addition to that, we also managed to fix some wonky vertical shifting due to the control’s built in StatusBar compensation and we did it all through XAML without a single line of code needed. Here’s all of the xaml:

<Thickness x:Key="PivotPortraitThemePadding">19,12,0,0</Thickness>
<Thickness x:Key="PivotLandscapeThemePadding">19,18,0,0</Thickness>
<x:Int32 x:Key="PivotHeaderItemCharacterSpacing">0</x:Int32>
<!--Use 19,16,19,0 thickness if you prefer the default left/right margins-->
<!--<Thickness x:Key="PivotItemMargin">19,16,19,0</Thickness>-->
<Thickness x:Key="PivotItemMargin">0,16,0,0</Thickness>
<Thickness x:Key="PivotHeaderItemMargin">19,-6.5,5,0</Thickness>
<Style TargetType="Pivot">
    <Setter Property="HeaderTemplate">
        <Setter.Value>
            <DataTemplate>
                <TextBlock Text="{Binding}"
                           OpticalMarginAlignment="TrimSideBearings" />
            </DataTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Pivot">
                <Grid HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                      VerticalAlignment="{TemplateBinding VerticalAlignment}"
                      Background="{TemplateBinding Background}">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="Orientation">
                            <VisualState x:Name="Portrait">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl"
                                                                   Storyboard.TargetProperty="Margin">
                                        <DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource PivotPortraitThemePadding}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Landscape">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl"
                                                                   Storyboard.TargetProperty="Margin">
                                        <DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource PivotLandscapeThemePadding}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>
                    <ContentControl x:Name="TitleContentControl"
                                    Style="{StaticResource PivotTitleContentControlStyle}"
                                    Content="{TemplateBinding Title}"
                                    ContentTemplate="{TemplateBinding TitleTemplate}" />
                    <ScrollViewer x:Name="ScrollViewer"
                                  Margin="{TemplateBinding Padding}"
                                  Grid.Row="1"
                                  HorizontalSnapPointsType="MandatorySingle"
                                  HorizontalSnapPointsAlignment="Center"
                                  HorizontalScrollBarVisibility="Hidden"
                                  VerticalScrollMode="Disabled"
                                  VerticalScrollBarVisibility="Disabled"
                                  VerticalSnapPointsType="None"
                                  VerticalContentAlignment="Stretch"
                                  ZoomMode="Disabled"
                                  Template="{StaticResource ScrollViewerScrollBarlessTemplate}">
                        <PivotPanel x:Name="Panel"
                                               VerticalAlignment="Stretch">
                            <PivotHeaderPanel x:Name="Header">
                                <PivotHeaderPanel.RenderTransform>
                                    <CompositeTransform x:Name="HeaderTranslateTransform"
                                                        TranslateX="0" />
                                </PivotHeaderPanel.RenderTransform>
                            </PivotHeaderPanel>
                            <ItemsPresenter x:Name="PivotItemPresenter">
                                <ItemsPresenter.RenderTransform>
                                    <TranslateTransform x:Name="ItemsPresenterTranslateTransform"
                                                        X="0" />
                                </ItemsPresenter.RenderTransform>
                            </ItemsPresenter>
                        </PivotPanel>
                    </ScrollViewer>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
Advertisements

One thought on “Fixing Pivot Control Layout Issues

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s