问题
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:
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:
The header background image is:
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>
回答1:
So with some assistance from @sohcatt on the Windows UI Dev Labs issues list, I have built a working solution.
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);
}
回答2:
<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.
回答3:
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:
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
.
来源:https://stackoverflow.com/questions/38361081/uwp-composition-apply-opacity-mask-to-top-30px-of-a-listview