Композиция UWP - применить маску непрозрачности к верхним 30 пикселям ListView

Как я могу применить эффект к ListView, где верхние 30px переходят от полностью прозрачного к полностью непрозрачному? Идея заключается в том, что при прокрутке верхние элементы постепенно исчезают.

Я создаю приложение UWP, в котором дизайн требует, чтобы верхние 30 пикселей ListView начинались с непрозрачности 0 и переходили к непрозрачности 1. Концептуально я представляю маску непрозрачности, которая будет применена к верхней части SpriteVisual, но я не могу работать как этого добиться.

Я пытаюсь сделать это, используя юбилейную версию Windows 10, Composition и Win2D.

Изменить: изображение может нарисовать 1000 слов:

Пример затухания

Если вы посмотрите на это изображение, в левом нижнем и правом нижнем углу будут два элемента содержимого. Хотя фон выглядит черным, на самом деле это градиент. Если вы изучите верхнюю часть двух элементов, они станут более прозрачными к вершине, показывая сквозь фон. Это тот эффект, которого я пытаюсь достичь.

Изменить 2: В попытке показать результат эффекта, который я ищу, вот GIF, который показывает эффект, если я использую наложенные растровые изображения: Прокрутка изображений

Фоновое изображение заголовка: фон заголовка

Нижние 30 пикселей имеют альфа-градиент и отображаются над видом сетки, что дает видимый эффект исчезновения элементов вида сетки и скольжения под фоном.

Макет 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>

3 ответа

Решение

Итак, с некоторой помощью @sohcatt в списке проблем Windows UI Dev Labs, я создал рабочее решение.

Вот 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);
    }
    <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, который переходит от полной белизны к полной прозрачности. Попытайтесь поместить это в свой просмотр списка и посмотрите, идет ли это хорошо.

Как я пытался объяснить ранее - фон не является однородным сплошным цветом - это изображение, которое меняется.

Я думаю, одну вещь, которую мы должны знать, это то, что по умолчанию 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 внутри, чтобы достичь желаемого макета. В этой сетке, конечно же, ListView должно занимать все пространство, но мы можем отделить это Grid на две части, одна часть для LinearGradientBrush, а другой для фона ListView, Вот изображение рендеринга этого макета:

И если вы хотите установить другое изображение в качестве фона для ListViewЯ думаю, что мы можем получить только средний цвет этого изображения и связать GradientStop из Offset = 1 к этому цвету.

Обновить

На переднем плане 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,

Другие вопросы по тегам