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

3

7

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>

Determinative answered 13/7, 2016 at 20:24 Comment(3)
have you thought about how the first item is supposed to look like if it is half way invisible?Scavenge
That's not really the point of the question - I'm implementing a design that exists on a number of different platforms, so how it looks is neither here nor there. I imagine the designer considers the effect to be akin to the top item in the list "sliding beneath" another element, with the lower boundary of that element starting as transparent and becoming fully opaque. The reality is, that is exactly how I would do it if the theoretical top-level element was static, but the background image changes constantly, so rather than constantly cutting images, I'd like to leverage composition.Determinative
Alas that refers to the WPF OpacityMask capability which is missing from UWP - that's what I am hoping to achieve with composition.Determinative
D
4

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);
    }
Determinative answered 30/9, 2016 at 21:19 Comment(0)
S
0
    <Grid Height="30"
          VerticalAlignment="Top">
        <Grid.Background>
            <LinearGradientBrush EndPoint="0.5,1"
                                 StartPoint="0.5,0">
                <GradientStop Color="White"
                              Offset="0" />
                <GradientStop Color="Transparent"
                              Offset="1" />
            </LinearGradientBrush>
        </Grid.Background>
    </Grid>

The code above creates a 30px gradient that goes updown from total whiteness to total transparency. Try to put it on your listview and see if it goes well.

Seaddon answered 13/7, 2016 at 21:4 Comment(3)
Unfortunately I don't wish to "fade to white".Determinative
i know the white color is an example, you should give it one that matches the background of your page/listview so that it appears fading awaySeaddon
As I tried to explain earlier - the background is not a consistent solid color - it is an image that changes.Determinative
L
0

As I tried to explain earlier - the background is not a consistent solid color - it is an image that changes.

I think one thing we should know is that by default ListView control's background is transparent. So if ListView's parent control is set a image as background, to achieve your desired layout, we need set another background for ListView, and meanwhile this background can't fill the whole ListView.

So, here is a method:

<Grid>
    <Grid.Background>
        <ImageBrush ImageSource="Assets/background.png" />
    </Grid.Background>
    <Grid Margin="0,100">
        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <Grid.Background>
                <LinearGradientBrush EndPoint="0.5,1"
                             StartPoint="0.5,0">
                    <GradientStop Color="Transparent"
                          Offset="0" />
                    <GradientStop Color="Wheat"
                          Offset="1" />
                </LinearGradientBrush>
            </Grid.Background>
        </Grid>
        <Grid Grid.Row="1" Background="Wheat" />
        <ListView ItemsSource="{x:Bind listCollection}" Grid.RowSpan="2">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding testText}" FontSize="20" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Grid>

As you can see in this code, I set a image as background for the rootGrid, and put another Grid inside to achieve your desired layout. In this Grid, of course the ListView should take all the space, but we can separate this Grid into two parts, one part is for LinearGradientBrush, and the other one is for background of the ListView. Here is the rendering image of this layout: enter image description here

And if you want to set another image as background for ListView, I think we can only get the average color of this image and bind the GradientStop of Offset = 1 to this color.

Update

For the foreground of the ListView, I think you are right, we need to cover a mask over it. Here is a method:

<Grid>
    <Grid.Background>
        <ImageBrush ImageSource="Assets/background.png" />
    </Grid.Background>
    <Grid Margin="0,100">
        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <Grid.Background>
                <LinearGradientBrush EndPoint="0.5,1"
                             StartPoint="0.5,0">
                    <GradientStop Color="Transparent"
                          Offset="0" />
                    <GradientStop Color="Wheat"
                          Offset="1" />
                </LinearGradientBrush>
            </Grid.Background>
        </Grid>
        <Grid Grid.Row="1" Background="Wheat" />
        <ListView ItemsSource="{x:Bind listCollection}" Grid.RowSpan="2">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding testText}" FontSize="20" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Grid Grid.Row="0">
            <Grid.Background>
                <LinearGradientBrush EndPoint="0.5,1"
                             StartPoint="0.5,0">
                    <GradientStop Color="Transparent"
                          Offset="0" />
                    <GradientStop Color="Wheat"
                          Offset="1" />
                </LinearGradientBrush>
            </Grid.Background>
        </Grid>
    </Grid>
</Grid>

There is a problem here, by default the scrollbar of the ListView can be seen and when using a mask over it, the scrollbar will also be covered. To achieve a better layout, it's better to set ScrollViewer.VerticalScrollBarVisibility="Hidden" to the ListView.

Lothaire answered 14/7, 2016 at 8:45 Comment(6)
Grace - this a close approximation of the effect. The issues are these:Determinative
Grace - this a close approximation of the effect - thank you. Unfortunately, there are issues. The foreground color stay fully opaque, so still appears to be above the background. The body of the list still needs to show the background. Each item is movie poster, arranged horizontally with margin that shows the background. The only way the effect can be achieved dynamically would be by applying some form of alpha mask to the top of the composition output, but I can't work out how to do this.Determinative
I've tried to build a Sprite visual using composition - creating a backdrop and applying a composite effect utilizing a mask, how I have been unable to achieve my desired result,Determinative
@DarenMay, I updated my answer. It works, but I actually don't understand what you mean by building a sprite visual using composition. Maybe you can show me some light to use other way to solve this problem.Lothaire
UI composition : blogs.windows.com/buildingapps/2015/12/08/…Determinative
Look at the foreground focus effect sample and you can see the effects that are being applied to a list view.Determinative

© 2022 - 2024 — McMap. All rights reserved.