I know this is an older question, but I came across this question because I was trying to do something similar; so I figured I'd post my solution for the next person. Any feedback on my solution is appreciated.
In our application, most of our ScrollViewer controls sit on top of non-scrolling textures, so we wanted the scrollable content to fade into that background at the edges of the ScrollViewer, but only when there was more content in that direction. In addition, we have at least one 2-axis scrollable area where the user can pan around in every direction. It had to work in that scenario as well. Our application also doesn't really have scrollbars, but I've left that out of the solution I present here (it doesn't impact the solution).
Features of this solution:
Fades the edges of the content within the ScrollViewer if there is content along that side of the ScrollViewer that is not currently visible.
Decreases the intensity of the fade effect as you scroll closer to the edge of the content.
Gives some control over how the faded edges look. Specifically, you can control:
- Thickness of the faded edge
- How opaque the content is at the outermost edge (or how "intense" the fade is)
- How fast the fade effect disappears as you scroll near the edge
The basic idea is to control an opacity mask over the scrollable content in the ScrollViewer's template. The opacity mask contains a transparent outer border, and an inner opaque border with the BlurEffect applied to it to get the gradient effect at the edges. Then, the margin of the inner border is manipulated as you scroll around to control how "deep" the fade appears along a particular edge.
This solution subclasses the ScrollViewer, and requires you to specify a change to the ScrollViewer's template. The ScrollContentPresenter needs to be wrapped inside a Border named "PART_ScrollContentPresenterContainer".
The FadingScrollViewer class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Effects;
namespace ScrollViewerTest
{
public class FadingScrollViewer : ScrollViewer
{
private const string PART_SCROLL_PRESENTER_CONTAINER_NAME = "PART_ScrollContentPresenterContainer";
public double FadedEdgeThickness { get; set; }
public double FadedEdgeFalloffSpeed { get; set; }
public double FadedEdgeOpacity { get; set; }
private BlurEffect InnerFadedBorderEffect { get; set; }
private Border InnerFadedBorder { get; set; }
private Border OuterFadedBorder { get; set; }
public FadingScrollViewer()
{
this.FadedEdgeThickness = 20;
this.FadedEdgeFalloffSpeed = 4.0;
this.FadedEdgeOpacity = 0.0;
this.ScrollChanged += FadingScrollViewer_ScrollChanged;
this.SizeChanged += FadingScrollViewer_SizeChanged;
}
private void FadingScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (this.InnerFadedBorder == null)
return;
var topOffset = CalculateNewMarginBasedOnOffsetFromEdge(this.VerticalOffset); ;
var bottomOffset = CalculateNewMarginBasedOnOffsetFromEdge(this.ScrollableHeight - this.VerticalOffset);
var leftOffset = CalculateNewMarginBasedOnOffsetFromEdge(this.HorizontalOffset);
var rightOffset = CalculateNewMarginBasedOnOffsetFromEdge(this.ScrollableWidth - this.HorizontalOffset);
this.InnerFadedBorder.Margin = new Thickness(leftOffset, topOffset, rightOffset, bottomOffset);
}
private double CalculateNewMarginBasedOnOffsetFromEdge(double edgeOffset)
{
var innerFadedBorderBaseMarginThickness = this.FadedEdgeThickness / 2.0;
var calculatedOffset = (innerFadedBorderBaseMarginThickness) - (1.5 * (this.FadedEdgeThickness - (edgeOffset / this.FadedEdgeFalloffSpeed)));
return Math.Min(innerFadedBorderBaseMarginThickness, calculatedOffset);
}
private void FadingScrollViewer_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (this.OuterFadedBorder == null || this.InnerFadedBorder == null || this.InnerFadedBorderEffect == null)
return;
this.OuterFadedBorder.Width = e.NewSize.Width;
this.OuterFadedBorder.Height = e.NewSize.Height;
double innerFadedBorderBaseMarginThickness = this.FadedEdgeThickness / 2.0;
this.InnerFadedBorder.Margin = new Thickness(innerFadedBorderBaseMarginThickness);
this.InnerFadedBorderEffect.Radius = this.FadedEdgeThickness;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
BuildInnerFadedBorderEffectForOpacityMask();
BuildInnerFadedBorderForOpacityMask();
BuildOuterFadedBorderForOpacityMask();
SetOpacityMaskOfScrollContainer();
}
private void BuildInnerFadedBorderEffectForOpacityMask()
{
this.InnerFadedBorderEffect = new BlurEffect()
{
RenderingBias = RenderingBias.Performance,
};
}
private void BuildInnerFadedBorderForOpacityMask()
{
this.InnerFadedBorder = new Border()
{
Background = Brushes.Black,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
VerticalAlignment = System.Windows.VerticalAlignment.Stretch,
Effect = this.InnerFadedBorderEffect,
};
}
private void BuildOuterFadedBorderForOpacityMask()
{
byte fadedEdgeByteOpacity = (byte)(this.FadedEdgeOpacity * 255);
this.OuterFadedBorder = new Border()
{
Background = new SolidColorBrush(Color.FromArgb(fadedEdgeByteOpacity, 0, 0, 0)),
ClipToBounds = true,
Child = this.InnerFadedBorder,
};
}
private void SetOpacityMaskOfScrollContainer()
{
var opacityMaskBrush = new VisualBrush()
{
Visual = this.OuterFadedBorder
};
var scrollContentPresentationContainer = this.Template.FindName(PART_SCROLL_PRESENTER_CONTAINER_NAME, this) as UIElement;
if (scrollContentPresentationContainer == null)
return;
scrollContentPresentationContainer.OpacityMask = opacityMaskBrush;
}
}
}
Here's the XAML to use the control, with the most minimal changes to the default ScrollViewer template required (it's the Border around the ScrollContentPresenter).
<local:FadingScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible" Margin="10" FadedEdgeThickness="20" FadedEdgeOpacity="0" FadedEdgeFalloffSpeed="4">
<local:FadingScrollViewer.Template>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid x:Name="Grid" Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border x:Name="PART_ScrollContentPresenterContainer">
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" CanHorizontallyScroll="False" CanVerticallyScroll="False" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Grid.Column="0" Margin="{TemplateBinding Padding}" Grid.Row="0"/>
</Border>
<ScrollBar x:Name="PART_VerticalScrollBar" AutomationProperties.AutomationId="VerticalScrollBar" Cursor="Arrow" Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Grid.Row="0" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportHeight}"/>
<ScrollBar x:Name="PART_HorizontalScrollBar" AutomationProperties.AutomationId="HorizontalScrollBar" Cursor="Arrow" Grid.Column="0" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Orientation="Horizontal" Grid.Row="1" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportWidth}"/>
</Grid>
</ControlTemplate>
</local:FadingScrollViewer.Template>
<!-- Your content here -->
</local:FadingScrollViewer>
Note these additional properties on the FadedScrollViewer: FadedEdgeThickness, FadedEdgeOpacity, and FadedEdgeFalloffSpeed
- FadedEdgeThickness: How thick do you want the fade to be (in pixels)
- FadedEdgeOpacity: How opaque do you want the outer-most edge of the fade to be. 0 = completely transparent at the edge, 1 = do not fade at all at the edge
- FadedEdgeFalloffSpeed: Controls how fast the faded edge appears to disappear as you get close to it. The higher the value, the slower the fade out.