[UWP] FlyoutView control

In a project, I needed a control like the old SettingsFlyout which is now obselete. What I wanted is just a panel that come-and-go with a great animation.

Style


<Style TargetType="flyoutView:FlyoutView">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="flyoutView:FlyoutView">
                <Grid>
                    <Grid x:Name="PART_OVERLAY" Background="{TemplateBinding BackgroundOverlay}" Visibility="Collapsed" />

                    <Grid x:Name="PART_CONTENT">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition />
                        </Grid.RowDefinitions>

                        <Grid.RenderTransform>
                            <CompositeTransform />
                        </Grid.RenderTransform>

                        <ContentControl ContentTemplate="{TemplateBinding HeaderTemplate}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" />

                        <ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The logic

We need to create the storyboards by hand because the content width/height aren’t static.

[TemplatePart(Name = PartOverlay, Type = typeof(Grid))]
[TemplatePart(Name = PartContent, Type = typeof(Grid))]
public partial class FlyoutView : ContentControl
{
    #region Dependency properties

    public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.Register(
            "HeaderTemplate", typeof(DataTemplate), typeof(FlyoutView),
            new PropertyMetadata(default(DataTemplate)));

    public DataTemplate HeaderTemplate
    {
        get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
        set { SetValue(HeaderTemplateProperty, value); }
    }

    public static readonly DependencyProperty PlacementProperty = DependencyProperty.Register(
        "Placement", typeof(PlacementType), typeof(FlyoutView),
        new PropertyMetadata(default(PlacementType), OnPlacementChanged));

    public PlacementType Placement
    {
        get { return (PlacementType)GetValue(PlacementProperty); }
        set { SetValue(PlacementProperty, value); }
    }

    public static readonly DependencyProperty UseDismissOvelayProperty = DependencyProperty.Register(
        "UseDismissOvelay", typeof(bool), typeof(FlyoutView),
        new PropertyMetadata(default(bool)));

    public bool UseDismissOvelay
    {
        get { return (bool)GetValue(UseDismissOvelayProperty); }
        set { SetValue(UseDismissOvelayProperty, value); }
    }

    public static readonly DependencyProperty BackgroundOverlayProperty = DependencyProperty.Register(
        "BackgroundOverlay", typeof(SolidColorBrush), typeof(FlyoutView),
        new PropertyMetadata(default(SolidColorBrush)));

    public SolidColorBrush BackgroundOverlay
    {
        get { return (SolidColorBrush)GetValue(BackgroundOverlayProperty); }
        set { SetValue(BackgroundOverlayProperty, value); }
    }

    public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register(
            "IsOpen", typeof(bool), typeof(FlyoutView),
            new PropertyMetadata(default(bool), OnIsOpenChanged));

    public bool IsOpen
    {
        get { return (bool)GetValue(IsOpenProperty); }
        set { SetValue(IsOpenProperty, value); }
    }

    public static readonly DependencyProperty ContentWidthProperty = DependencyProperty.Register(
        "ContentWidth", typeof(double), typeof(FlyoutView),
        new PropertyMetadata(default(double)));

    public double ContentWidth
    {
        get { return (double)GetValue(ContentWidthProperty); }
        set { SetValue(ContentWidthProperty, value); }
    }

    public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(
        "ContentHeight", typeof(double), typeof(FlyoutView),
        new PropertyMetadata(default(double)));

    public double ContentHeight
    {
        get { return (double)GetValue(ContentHeightProperty); }
        set { SetValue(ContentHeightProperty, value); }
    }

    #endregion

    #region Properties

    public enum PlacementType
    {

        Left,
        Top,
        Right,
        Bottom
    }

    private const string PartOverlay = "PART_OVERLAY";
    private const string PartContent = "PART_CONTENT";

    private Storyboard _openStoryboard;
    private Storyboard _closeStoryboard;
    private Grid _content;
    private Grid _overlay;

    #endregion

    #region Constructor

    public FlyoutView()
    {
        DefaultStyleKey = typeof(FlyoutView);
    }

    #endregion

    #region Override

    protected override void OnApplyTemplate()
    {
        _overlay = GetTemplateChild(PartOverlay) as Grid;
        _content = GetTemplateChild(PartContent) as Grid;

        if (_content != null)
        {
            _content.Loaded += OnContentLoaded;
            HandleOpening();
        }

        if (_overlay != null)
            _overlay.Tapped += OnOverlayTapped;

        UpdateFromLayout();

        base.OnApplyTemplate();
    }

    #endregion

    #region Methods

    private void UpdateFromLayout()
    {
        if (_content == null || _overlay == null)
            return;

        double overflow;
        if (Placement == PlacementType.Left
            || Placement == PlacementType.Right)
            overflow = Placement == PlacementType.Left
                ? -ContentWidth
                : ContentWidth;
        else
            overflow = Placement == PlacementType.Top
                ? -ContentHeight
                : ContentHeight;

        UpdatePlacement(overflow);
        UpdateAnimations(overflow);
    }

    private void UpdatePlacement(double overflow)
    {
        if (Placement == PlacementType.Left
            || Placement == PlacementType.Right)
        {
            _content.Width = ContentWidth;
            _content.Height = double.NaN;
            _content.RenderTransform = new CompositeTransform { TranslateX = overflow, TranslateY = 0 };
            _content.HorizontalAlignment = Placement == PlacementType.Left
                ? HorizontalAlignment.Left
                : HorizontalAlignment.Right;
            _content.VerticalAlignment = VerticalAlignment.Stretch;
        }
        else
        {
            _content.Width = double.NaN;
            _content.Height = ContentHeight;
            _content.RenderTransform = new CompositeTransform { TranslateX = 0, TranslateY = overflow };
            _content.VerticalAlignment = Placement == PlacementType.Top
                ? VerticalAlignment.Top
                : VerticalAlignment.Bottom;
            _content.HorizontalAlignment = HorizontalAlignment.Stretch;
        }
    }

    private void UpdateAnimations(double overflow)
    {
        var targetProperty = Placement == PlacementType.Left || Placement == PlacementType.Right
            ? "(UIElement.RenderTransform).(CompositeTransform.TranslateX)"
            : "(UIElement.RenderTransform).(CompositeTransform.TranslateY)";

        // Create animations
        _openStoryboard = new Storyboard();
        var openAnimation = new DoubleAnimation
        {
            Duration = TimeSpan.FromMilliseconds(500),
            From = overflow,
            To = 0,
            EasingFunction = new PowerEase { EasingMode = EasingMode.EaseIn, Power = 2 }
        };
        Storyboard.SetTargetProperty(openAnimation, targetProperty);
        Storyboard.SetTarget(openAnimation, _content);
        _openStoryboard.Children.Add(openAnimation);

        _closeStoryboard = new Storyboard();
        var closeAnimation = new DoubleAnimation
        {
            Duration = TimeSpan.FromMilliseconds(500),
            From = 0,
            To = overflow,
            EasingFunction = new PowerEase { EasingMode = EasingMode.EaseOut, Power = 2 }
        };
        Storyboard.SetTargetProperty(closeAnimation, targetProperty);
        Storyboard.SetTarget(closeAnimation, _content);
        _closeStoryboard.Children.Add(closeAnimation);
    }

    #endregion

    #region Events

    private void OnContentLoaded(object sender, object o)
    {
        _content.Loaded -= OnContentLoaded;

        UpdateFromLayout();
    }

    private void OnOverlayTapped(object sender, TappedRoutedEventArgs e)
    {
        if (IsOpen)
            IsOpen = false;
    }

    private static void OnIsOpenChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        var control = sender as FlyoutView;

        control?.HandleOpening();
    }

    private void HandleOpening()
    {
        if (IsOpen)
        {
            if (UseDismissOvelay)
                _overlay.Visibility = Visibility.Visible;

            _openStoryboard?.Begin();
        }
        else
        {
            if (UseDismissOvelay)
                _overlay.Visibility = Visibility.Collapsed;

            _closeStoryboard?.Begin();
        }
    }

    private static void OnPlacementChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        var control = sender as FlyoutView;

        control?.UpdateFromLayout();
    }

    #endregion
}

Usage

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Image Width="300" Height="300" Source="ms-appx:///Assets/ToolkitLogo.png" />

    <flyoutView:FlyoutView IsOpen="{Binding Path=IsOpen.Value, Mode=TwoWay}" Placement="{Binding Path=Placement.Value, Mode=TwoWay}" UseDismissOvelay="True" BackgroundOverlay="#BBF0F0F0" ContentWidth="300" ContentHeight="150">
        <flyoutView:FlyoutView.HeaderTemplate>
            <DataTemplate>
                <Grid Background="{StaticResource Brush-Grey-02}">
                    <TextBlock Text="{Binding Path=Header.Value}" Style="{StaticResource HeaderTextBlockStyle}" Margin="20" />
                </Grid>
            </DataTemplate>
        </flyoutView:FlyoutView.HeaderTemplate>

        <Grid Background="{StaticResource Brush-Grey-03}">
            <TextBlock HorizontalAlignment="Center" TextWrapping="Wrap" Text="This is the content" VerticalAlignment="Center" Style="{StaticResource SubtitleTextBlockStyle}" />
        </Grid>
    </flyoutView:FlyoutView>
</Grid>

Demo

Leave a Reply