Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UWP Composition - Apply opacity mask to top 30px of a ListView

How can I apply an effect to a ListView where the top 30px graduate from fully transparent to fully opaque? The idea is that as you scroll down, the top items gradually fade away.

I am building a UWP application where the design calls for the top 30px of a ListView to start at opacity 0 and transition to opacity 1. Conceptually I am imagining an Opacity Mask that would be applied to the top part of a SpriteVisual but I cannot work out how to achieve this.

I am attempting this using the the anniversary edition of Windows 10, Composition and Win2D.

Edit: a picture may paint a 1000 words:

Example of the fade

If you look at this image, there are two content elements in the bottom-left and bottom-right. Although the background appears to be black, it is actually a gradient. If you examine the top of the two elements, they become more transparent towards the top, showing through the background. That is the effect I am trying to achieve.

Edit 2: In an attempt to show the outcome of the effect I am looking for, here is a GIF that shows the effect if I use overlaid bitmaps: Scrolling Images

The header background image is: header background

The lower 30px has an alpha gradient and appears above the gridview giving the apparent effect of the grid view items fading out and sliding undernext the background.

The XAML layout looks like:

<Page
x:Class="App14.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App14"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="150" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <Image Source="/Assets/background.png"
           Grid.Row="0"
           Grid.RowSpan="2"
           VerticalAlignment="Top"
           Stretch="None" />

    <GridView Grid.Row="1"
              Margin="96,-30,96,96">
        <GridView.Resources>
            <Style TargetType="Image">
                <Setter Property="Height" Value="400" />
                <Setter Property="Width" Value="300" />
                <Setter Property="Margin" Value="30" />
            </Style>
        </GridView.Resources>
        <Image Source="Assets/1.jpg" />
        <Image Source="Assets/2.jpg" />
        <Image Source="Assets/3.jpg" />
        <Image Source="Assets/4.jpg" />
        <Image Source="Assets/5.jpg" />
        <Image Source="Assets/6.jpg" />
        <Image Source="Assets/7.jpg" />
        <Image Source="Assets/8.jpg" />
        <Image Source="Assets/9.jpg" />
        <Image Source="Assets/10.jpg" />
        <Image Source="Assets/11.jpg" />
        <Image Source="Assets/12.jpg" />
    </GridView>

    <!-- Header above content -->

    <Image Grid.Row="0" Source="/Assets/header_background.png"
           Stretch="None" />

    <TextBlock x:Name="Title"
               Grid.Row="0"
               FontSize="48"
               Text="This Is A Title"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               Foreground="White" />


</Grid>

like image 988
DarenMay Avatar asked Jul 13 '16 20:07

DarenMay


1 Answers

So with some assistance from @sohcatt on the Windows UI Dev Labs issues list, I have built a working solution.

enter image description here

Here is the XAML:

    <Grid x:Name="LayoutRoot">

    <Image x:Name="BackgroundImage"
           ImageOpened="ImageBrush_OnImageOpened"
           Source="../Assets/blue-star-background-wallpaper-3.jpg"
           Stretch="UniformToFill" />

    <GridView x:Name="Posters" Margin="200,48">
        <GridView.Resources>
            <Style TargetType="ListViewItem" />
            <Style TargetType="Image">
                <Setter Property="Stretch" Value="UniformToFill" />
                <Setter Property="Width" Value="300" />
                <Setter Property="Margin" Value="12" />
            </Style>
        </GridView.Resources>
        <GridViewItem>
            <Image Source="Assets/Posters/1.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/2.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/3.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/4.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/5.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/6.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/7.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/8.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/9.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/10.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/11.jpg" />
        </GridViewItem>
        <GridViewItem>
            <Image Source="Assets/Posters/12.jpg" />
        </GridViewItem>
    </GridView>
</Grid>

Here is the code:

        private bool _imageLoaded;

    // this is an initial way of handling resize 
    // I will investigate expressions
    private async void OnSizeChanged(object sender, SizeChangedEventArgs args)
    {
        if (!_imageLoaded)
        {
            return;
        }
        await RenderOverlayAsync();
    }

    private async void ImageBrush_OnImageOpened(object sender, RoutedEventArgs e)
    {
        _imageLoaded = true;
        await RenderOverlayAsync();
    }

    // this method must be called after the background image is opened, otherwise
    // the render target bitmap is empty
    private async Task RenderOverlayAsync()
    {
        // setup composition
        // (in line here for readability - will be member variables moving forwards)
        var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
        var canvasDevice = new CanvasDevice();
        var compositionDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice);

        // determine what region of the background we need to "cut out" for the overlay
        GeneralTransform gt = Posters.TransformToVisual(LayoutRoot);
        Point elementPosition = gt.TransformPoint(new Point(0, 0));

        // our overlay height is as wide as our poster control and is 30 px high
        var overlayHeight = 30;
        var areaToRender = new Rect(elementPosition.X, elementPosition.Y, Posters.ActualWidth, overlayHeight);

        // Capture the image from our background.
        //
        // Note: this is just the <Image/> element, not the Grid. If we took the <Grid/>, 
        // we would also have all of the child elements, such as the <GridView/> rendered as well -
        // which defeats the purpose!
        // 
        // Note 2: this method must be called after the background image is opened, otherwise
        // the render target bitmap is empty
        var bitmap = new RenderTargetBitmap();
        await bitmap.RenderAsync(BackgroundImage);
        var pixels = await bitmap.GetPixelsAsync();

        // we need the display DPI so we know how to handle the bitmap correctly when we render it
        var dpi = DisplayInformation.GetForCurrentView().LogicalDpi;

        // load the pixels from RenderTargetBitmap onto a CompositionDrawingSurface
        CompositionDrawingSurface uiElementBitmapSurface;
        using (
            // this is the entire background image
            // Note we are using the display DPI here.
            var canvasBitmap = CanvasBitmap.CreateFromBytes(
                canvasDevice, pixels.ToArray(),
                bitmap.PixelWidth,
                bitmap.PixelHeight,
                DirectXPixelFormat.B8G8R8A8UIntNormalized,
                dpi)
        )
        {
            // we create a surface we can draw on in memory.
            // note we are using the desired size of our overlay
            uiElementBitmapSurface =
                compositionDevice.CreateDrawingSurface(
                    new Size(areaToRender.Width, areaToRender.Height),
                    DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
            using (var session = CanvasComposition.CreateDrawingSession(uiElementBitmapSurface))
            {
                // here we draw just the part of the background image we wish to use to overlay
                session.DrawImage(canvasBitmap, 0, 0, areaToRender);
            }
        }

        // assign CompositionDrawingSurface to the CompositionSurfacebrush with which I want to paint the relevant SpriteVisual
        var backgroundImageBrush = _compositor.CreateSurfaceBrush(uiElementBitmapSurface);

        // load in our opacity mask image.
        // this is created in a graphic tool such as paint.net
        var opacityMaskSurface = await SurfaceLoader.LoadFromUri(new Uri("ms-appx:///Assets/OpacityMask.Png"));

        // create surfacebrush with ICompositionSurface that contains the background image to be masked
        backgroundImageBrush.Stretch = CompositionStretch.UniformToFill;

        // create surfacebrush with ICompositionSurface that contains the gradient opacity mask asset
        CompositionSurfaceBrush opacityBrush = _compositor.CreateSurfaceBrush(opacityMaskSurface);
        opacityBrush.Stretch = CompositionStretch.UniformToFill;

        // create maskbrush
        CompositionMaskBrush maskbrush = _compositor.CreateMaskBrush();
        maskbrush.Mask = opacityBrush; // surfacebrush with gradient opacity mask asset
        maskbrush.Source = backgroundImageBrush; // surfacebrush with background image that is to be masked

        // create spritevisual of the approproate size, offset, etc.
        SpriteVisual maskSprite = _compositor.CreateSpriteVisual();
        maskSprite.Size = new Vector2((float)Posters.ActualWidth, overlayHeight);
        maskSprite.Brush = maskbrush; // paint it with the maskbrush

        // set the sprite visual as a child of the XAML element it needs to be drawn on top of
        ElementCompositionPreview.SetElementChildVisual(Posters, maskSprite);
    }
like image 82
DarenMay Avatar answered Nov 04 '22 21:11

DarenMay