I wanted something relatively simple to experiment with using some of the things that I’d picked up about the Visual Layer when writing these posts;

and from Rob’s posts;

and, specifically, I wanted to try and do a little bit more with interactions that I’d started playing with;

and so I thought I’d make a stab at a cheap reproduction of what I see with the Windows 10 lock-screen’s behaviour which (purely from staring at it) seems to;

Slide up with the user’s finger. Fade out the text it displays as it slides On completion of “enough” of a slide, hides the text and appears to both zoom and darken the lock screen image before displaying the logon box.

Here’s a screen capture of my attempt to date;

and you’ll probably notice that it’s far from perfect but (I hope) it captures a little of what the lock-screen does.

In experimenting with this, I used a Blank UWP app on SDK preview 14388 with Win2d.uwp referenced and I had a simple piece of XAML as my UI;

<Grid Background="Red" PointerPressed="OnPointerPressed" x:Name="xamlRootGrid"> <Image x:Name="xamlImage" Source="ms-appx:///Assets/lockImage.jpg" HorizontalAlignment="Left" VerticalAlignment="Top" Stretch="UniformToFill" /> <!-- this grid is here to provide an easy place to add a blur to the image behind it --> <Grid x:Name="xamlBlurPlaceHolder" /> <Grid x:Name="xamlContentPanel" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <StackPanel HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="48,0,0,48"> <TextBlock Text="09:00" FontFamily="Segoe UI Light" Foreground="White" FontSize="124" /> <TextBlock Margin="0,-30,0,0" Text="Thursday, 14th July" FontFamily="Segoe UI" Foreground="White" FontSize="48" /> <TextBlock Text="Jim's Birthday" Margin="0,48,0,0" FontFamily="Segoe UI Semibold" Foreground="White" FontSize="24" /> <TextBlock Text="Friday All Day" FontFamily="Segoe UI Semibold" Foreground="White" FontSize="24" /> </StackPanel> </Grid> </Grid>

and you’ll probably notice that I don’t quite have the fonts or spacing quite right but it’s an approximation and then I wrote some code behind to try and achieve what I wanted;

namespace App12 { using Microsoft.Graphics.Canvas.Effects; using System; using System.Numerics; using Windows.UI; using Windows.UI.Composition; using Windows.UI.Composition.Interactions; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Hosting; using Windows.UI.Xaml.Input; public static class VisualExtensions { public static Visual GetVisual(this UIElement element) { return (ElementCompositionPreview.GetElementVisual(element)); } } public sealed partial class MainPage : Page, IInteractionTrackerOwner { public MainPage() { this.InitializeComponent(); this.Loaded += OnLoaded; } void OnLoaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) { // The visual for our root grid this.rootGridVisual = this.xamlRootGrid.GetVisual(); // Keep hold of our compositor. this.compositor = this.rootGridVisual.Compositor; // The visual for the grid which contains our text content. this.contentPanelVisual = this.xamlContentPanel.GetVisual(); // And for the image this.imageVisual = this.xamlImage.GetVisual(); // Set up the centre point for scaling the image // TODO: need to alter this on resize? this.imageVisual.CenterPoint = new Vector3( (float)this.xamlRootGrid.ActualWidth / 2.0f, (float)this.xamlRootGrid.ActualHeight / 2.0f, 0); // Get the visual for the grid which sits in front of the image that I can use to blur the image this.blurPlaceholderVisual = this.xamlBlurPlaceHolder.GetVisual(); // Create the pieces needed to blur the image at a later point. this.CreateDarkenedVisualAndAnimation(); this.CreateInteractionTrackerAndSource(); // NB: Creating our animations here before the layout pass has gone by would seem // to be a bad idea so we defer it. That was the big learning of this blog post. } void CreateInteractionTrackerAndSource() { // Create an interaction tracker with an owner (this object) so that we get // callbacks when interesting things happen, this was a major learning for // me in this piece of code. this.interactionTracker = InteractionTracker.CreateWithOwner(this.compositor, this); // We're using the root grid as the source of our interactions. this.interactionSource = VisualInteractionSource.Create(this.rootGridVisual); // We only want to be able to move in the Y direction. this.interactionSource.PositionYSourceMode = InteractionSourceMode.EnabledWithoutInertia; // From 0 to the height of the root grid (TODO: recreate on resize) this.interactionTracker.MaxPosition = new Vector3(0, (float)this.xamlRootGrid.ActualHeight, 0); this.interactionTracker.MinPosition = new Vector3(0, 0, 0); // How far do you have to drag before you unlock? Let's say half way. this.dragThreshold = this.xamlRootGrid.ActualHeight / 2.0d; // Connect the source to the tracker. this.interactionTracker.InteractionSources.Add(this.interactionSource); } void CreateDarkenedVisualAndAnimation() { var darkenedSprite = this.compositor.CreateSpriteVisual(); var backdropBrush = this.compositor.CreateBackdropBrush(); // TODO: resize? darkenedSprite.Size = new Vector2( (float)this.xamlRootGrid.ActualWidth, (float)this.xamlRootGrid.ActualHeight); // I borrowed this effect definition from a Windows UI sample and // then tweaked it. using (var graphicsEffect = new ArithmeticCompositeEffect() { Name = "myEffect", Source1Amount = 0.0f, Source2Amount = 1.0f, Source1 = new ColorSourceEffect() { Name = "Base", Color = Color.FromArgb(255, 0, 0, 0), }, Source2 = new CompositionEffectSourceParameter("backdrop") }) { this.darkenImageAnimation = this.compositor.CreateScalarKeyFrameAnimation(); this.darkenImageAnimation.InsertKeyFrame(0.0f, 1.0f); this.darkenImageAnimation.InsertKeyFrame(0.0f, 0.6f); this.darkenImageAnimation.Duration = TimeSpan.FromMilliseconds(250); using (var factory = this.compositor.CreateEffectFactory(graphicsEffect, new string[] { "myEffect.Source2Amount" })) { this.mixedDarkeningBrush = factory.CreateBrush(); this.mixedDarkeningBrush.SetSourceParameter("backdrop", backdropBrush); darkenedSprite.Brush = this.mixedDarkeningBrush; } } ElementCompositionPreview.SetElementChildVisual(this.xamlBlurPlaceHolder, darkenedSprite); } void OnPointerPressed(object sender, PointerRoutedEventArgs e) { // First time around, create our animations. if (this.positionAnimation == null) { LazyCreateDeferredAnimations(); } if (e.Pointer.PointerDeviceType == Windows.Devices.Input.PointerDeviceType.Touch) { // we send this to the interaction tracker. this.interactionSource.TryRedirectForManipulation( e.GetCurrentPoint(this.xamlRootGrid)); } } void LazyCreateDeferredAnimations() { // opacity. this.opacityAnimation = this.compositor.CreateExpressionAnimation(); this.opacityAnimation.Expression = "1.0 - (tracker.Position.Y / (tracker.MaxPosition.Y - tracker.MinPosition.Y))"; this.opacityAnimation.SetReferenceParameter("tracker", this.interactionTracker); this.contentPanelVisual.StartAnimation("Opacity", this.opacityAnimation); // position. this.positionAnimation = this.compositor.CreateExpressionAnimation(); this.positionAnimation.Expression = "-tracker.Position"; this.positionAnimation.SetReferenceParameter("tracker", this.interactionTracker); this.contentPanelVisual.StartAnimation("Offset", this.positionAnimation); // scale for the background image when we "unlock" CubicBezierEasingFunction easing = this.compositor.CreateCubicBezierEasingFunction( new Vector2(0.5f, 0.0f), new Vector2(1.0f, 1.0f)); // this animation and its easing don't 'feel' right at all, needs some tweaking this.scaleAnimation = this.compositor.CreateVector3KeyFrameAnimation(); this.scaleAnimation.InsertKeyFrame(0.0f, new Vector3(1.0f, 1.0f, 1.0f), easing); this.scaleAnimation.InsertKeyFrame(0.2f, new Vector3(1.075f, 1.075f, 1.0f), easing); this.scaleAnimation.InsertKeyFrame(1.0f, new Vector3(1.1f, 1.1f, 1.1f), easing); this.scaleAnimation.Duration = TimeSpan.FromMilliseconds(500); } // From hereon in, these methods are the implementation of IInteractionTrackerOwner. public void CustomAnimationStateEntered( InteractionTracker sender, InteractionTrackerCustomAnimationStateEnteredArgs args) { } public void IdleStateEntered( InteractionTracker sender, InteractionTrackerIdleStateEnteredArgs args) { if (this.unlock) { // We make sure that the text disappears this.contentPanelVisual.Opacity = 0.0f; // We try and zoom the image a little. this.imageVisual.StartAnimation("Scale", this.scaleAnimation); // And darken it a little. this.mixedDarkeningBrush.StartAnimation("myEffect.Source2Amount", this.darkenImageAnimation); } else { sender.TryUpdatePosition(Vector3.Zero); } } public void InertiaStateEntered( InteractionTracker sender, InteractionTrackerInertiaStateEnteredArgs args) { } public void InteractingStateEntered( InteractionTracker sender, InteractionTrackerInteractingStateEnteredArgs args) { this.unlock = false; } public void RequestIgnored( InteractionTracker sender, InteractionTrackerRequestIgnoredArgs args) { } public void ValuesChanged( InteractionTracker sender, InteractionTrackerValuesChangedArgs args) { if (!this.unlock && (args.Position.Y > this.dragThreshold)) { this.unlock = true; } } bool unlock; double dragThreshold; InteractionTracker interactionTracker; VisualInteractionSource interactionSource; Visual rootGridVisual; Visual contentPanelVisual; Visual blurPlaceholderVisual; Compositor compositor; ExpressionAnimation positionAnimation; ExpressionAnimation opacityAnimation; ScalarKeyFrameAnimation darkenImageAnimation; CompositionEffectBrush mixedDarkeningBrush; Vector3KeyFrameAnimation scaleAnimation; Visual imageVisual; } }

What’s that code doing?

At start-up getting hold of a bunch of Visuals for the various XAML UI elements. creating a Visual (darkenedSprite) which lives in the Grid named xamlBlurPlaceHolder and which will effectively paint itself with a mixed combination of the colour black and the image which sits under it in the Z-order. creating an animation (darkenImageAnimation) which will change the balance between black/image when necessary. creating an interaction tracker and an interaction source to track the Y movement of the touch pointer up the screen within some limits. On pointer-pressed Creating an animation which will cause the text content to slide up the screen wired to the interaction tracker Creating an animation which will cause the text content to fade out wired to the interaction tracker Creating an animation which will later be used to scale the image as the lock-screen is dismissed (this could, perhaps, be done earlier) Passing the pointer event (if it’s touch) across to the interaction tracker

In building that out, I learned 2 main things. One was that things have changed since build 10586 and I need to read the Wiki site more carefully as talked about in this post.

The other was around how to trigger the ‘dismissal’ of my lock-screen at the point where the user’s touch point has travelled far enough up the screen.

I was puzzled by that for quite a while. I couldn’t figure out how I was meant to know what the interaction tracker was doing and I kept looking for events without finding any.

Equally, I couldn’t figure out how to debug what the interaction tracker was doing when my code didn’t work.

That changed when I came across IInteractionTrackerOwner and the InteractionTracker.CreateWithOwner() method. Whether I have this right or not, it let me plug code (and diagnostics) into the InteractionTracker and I used the ValueChanged method to try and work out when the user’s touch point has gone 50% of the way up the screen so that I can then dismiss the lock-screen.

I don’t dismiss it immediately though. Instead, I wait for the IdleStateEntered callback and in that code I try to take steps to;

Set the opacity of the text panel to 0 so that it disappears. Begin the animation on the image Visual so as to zoom it a little Begin the animation on the composite brush that I have so as to darken the image by mixing it with Black.

The other thing that I learned was that I don’t understand the lighting features of the Visual Layer well enough yet and that I need to explore them some more in isolation to try and work that out.

But, the main thing for me here was to learn about IInteractionTrackerOwner and hence sharing that here (even in this rough, experimental form).