Композиция 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
,