Visually Located

XAML and GIS

Creating a behavior to stretch header content when at the top of a scroller

I’ve been playing around a lot with my wife’s new iPhone a lot lately. One feature I love on some of the apps is when you reach the top of a page a header image will stretch out to indicate you are at the top of the page. This is a fun feature that’s super easy to add using a behavior.

DF7A04AC-B526-4CF7-9F90-FBF4447A113E

The behavior will focus on scaling the image up by a factor but only when the ScrollerViewer is being “stretched”.

public class StretchyHeaderBehavior : Behavior<FrameworkElement>
{
    private ScrollViewer _scroller;
 
    public double StretchyFactor
    {
        get { return (double)GetValue(ScaleFactorProperty); }
        set { SetValue(ScaleFactorProperty, value); }
    }
 
    public static readonly DependencyProperty ScaleFactorProperty = DependencyProperty.Register(
        nameof(StretchyFactor),
        typeof(double),
        typeof(StretchyHeaderBehavior),
        new PropertyMetadata(0.5));
 
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.SizeChanged += OnSizeChanged;
        _scroller = AssociatedObject.GetParentOfType<ScrollViewer>();
        if (_scroller == null)
        {
            AssociatedObject.Loaded += OnLoaded;
            return;
        }
        AssignEffect();
    }
 
    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        _scroller = AssociatedObject.GetParentOfType<ScrollViewer>();
        AssignEffect();
        AssociatedObject.Loaded -= OnLoaded;
    }
 
    private void OnSizeChanged(object sender, SizeChangedEventArgs e)
    {
        AssignEffect();
    }
 
    private void AssignEffect()
    {
        if (_scroller == null) return;
 
        CompositionPropertySet scrollerViewerManipulation = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(_scroller);
 
        var compositor = scrollerViewerManipulation.Compositor;
 
        // See documentation for Lerp and Clamp: 
        // https://msdn.microsoft.com/en-us/windows/uwp/graphics/composition-animation
        var scaleAnimation = compositor.CreateExpressionAnimation(
             "Lerp(1, 1+Amount, Clamp(ScrollManipulation.Translation.Y/50, 0, 1))");
        scaleAnimation.SetScalarParameter("Amount", (float)StretchyFactor);
        scaleAnimation.SetReferenceParameter("ScrollManipulation", scrollerViewerManipulation);
 
        var visual = ElementCompositionPreview.GetElementVisual(AssociatedObject);
        var backgroundImageSize = new Vector2((float)AssociatedObject.ActualWidth, (float)AssociatedObject.ActualHeight);
        visual.Size = backgroundImageSize;
 
        // CenterPoint defaults to the top left (0,0). We want the strecth to occur from the center
        visual.CenterPoint = new Vector3(backgroundImageSize / 2, 1);
        visual.StartAnimation("Scale.X", scaleAnimation);
        visual.StartAnimation("Scale.Y", scaleAnimation);
    }
}

You can find the behavior on my GitHub repo along with a sample project. The sample gif above was even combined with the ParallaxBehavior to give it a little extra fun!

Thanks to Neil Turner for helping come up with the name of the behavior!

Revisiting the ParallaxBehavior to work in both directions

In my last post I explained how to create a behavior that would provide a parallax effect on any control. I was playing with the behavior the other day and I wanted to reverse the scrolling of a header image from going down to going up. I switched the ParallaxMultiplier property from a negative number to a positive number and noticed that the image started to scroll off the screen.

0A6C47AE-9631-4C5C-AEAD-976C8653294D

This is not at all what I wanted. I want to see the image in the space provided, but scroll, or parallax, the image as I scroll the content. I want the image to scroll upwards so I can still see the top/center of the image as I scroll the page down.

To fix this I need to adjust the expression. Currently the expression is "ScrollManipulation.Translation.Y * ParallaxMultiplier". We need to move the image down as the scroller moves. To do this we can subtract the Y Translation of the scroller. But we only want to do this for a multiplier greater than zero.

ExpressionAnimation expression = compositor.CreateExpressionAnimation(ParallaxMultiplier > 0 
    ? "ScrollManipulation.Translation.Y * ParallaxMultiplier - ScrollManipulation.Translation.Y" 
    : "ScrollManipulation.Translation.Y * ParallaxMultiplier");

Now, when the multiplier is greater than zero, the parallax effect works properly.

CA5F7D2B-6384-4E8F-AD54-E84AA3B6E7FA

You can get an updated version of the behavior on my repo. This does “break” the sample that has the image as the background of text. But let’s be honest, that’s not a real scenario anyone would use.