UWP Composition - 将不透明蒙版应用于 ListView 的顶部 30px

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

如何将效果应用于顶部 30px 从完全透明变为完全不透明的 ListView?这个想法是,当您向下滚动时,顶部的项目逐渐消失。

我正在构建一个 UWP 应用程序,其中设计要求 ListView 的顶部 30px 从不透明度 0 开始并过渡到不透明度 1。从概念上讲,我正在想象一个将应用于顶部的不透明蒙版SpriteVisual 但我不知道如何实现它。

我正在尝试使用 Windows 10、Composition 和 Win2D 的周年纪念版。

编辑:一张图可画1000字:

如果你看这张图片,左下角和右下角有两个内容元素。虽然背景看起来是黑色,但实际上是渐变色。如果您检查这两个元素的顶部,它们在顶部变得更加透明,透过背景显示出来。这就是我想要达到的效果。

编辑 2: 为了显示我正在寻找的效果的结果,这里有一个 GIF,它显示了我使用叠加位图时的效果:

页眉背景图片为:

下方的 30px 具有 alpha 渐变并出现在 gridview 上方,给出了 grid view 项目淡出并在背景下滑动的明显效果。

XAML 布局如下所示:

<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>

    <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>

上面的代码创建了一个 30px 的渐变,从全白到全透明。尝试把它放在你的列表视图上,看看它是否顺利。

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

我认为我们应该知道的一件事是默认情况下 ListView 控件的背景是透明的。所以如果ListView的父控件设置了一个图片作为背景,为了达到你想要的布局,我们需要为ListView设置另一个背景,同时这个背景不能填满整个ListView.

所以,这里有一个方法:

<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>

如您在此代码中所见,我将一张图像设置为 rootGrid 的背景,并在其中放入另一个 Grid 以实现您想要的布局。在这个Grid中,当然ListView应该把所有的space,但是我们可以把这个Grid分成两部分,一部分给LinearGradientBrush,另一部分给LinearGradientBrush用于 ListView 的背景。这是此布局的渲染图像:

而如果你想为ListView设置另一张图片作为背景,我想我们只能获取这张图片的平均颜色并将Offset = 1GradientStop绑定到这个颜色。

更新

对于ListView的前景,我觉得你说得对,我们需要在上面盖上一层遮罩。这是一个方法:

<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>

这里有一个问题,默认情况下ListView的滚动条是可以看到的,在上面使用遮罩时,滚动条也会被覆盖。为了获得更好的布局,最好将 ScrollViewer.VerticalScrollBarVisibility="Hidden" 设置为 ListView

因此,在 Windows UI 开发实验室问题列表中 @sohcatt 的一些帮助下,我构建了一个可行的解决方案。

这里是 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>

代码如下:

        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);
    }