I know this is an old question, but I came across this question because I was trying to do something like this; so I decided that I would send a solution for the next person. Any feedback on my decision is welcome.
In our application, most of our ScrollViewer controls are on top of disjoint textures, so we wanted the scrollable content to disappear against the background along the edges of the ScrollViewer, but only when there was more content in that direction. In addition, we have at least one area with 2 axis scrolling, where the user can move in any direction. He had to work in this scenario. Our application also does not have scrollbars, but I left this solution from the solution presented here (this does not affect the solution).
Features of this solution:
Decreases the edges of the content in the ScrollViewer if there is content on the side of the ScrollViewer that is not currently displayed.
Reduces the intensity of the fading effect when scrolling closer to the edge of the content.
Gives some control over how faded edges look. In particular, you can manage:
- Faded Edge Thickness
- How much opaque content is at the farthest edge (or how “intense” fading)
- How quickly the fading effect disappears when scrolling near the edge
The main idea is to control the opacity mask over the scrollable content in the ScrollViewer template. The opacity mask contains a transparent outer border and an inner opaque border with BlurEffect applied to it to get a gradient effect around the edges. Then, the edge of the inner border is manipulated while scrolling to control how “deep” attenuation appears along a certain edge.
This solution subclasses ScrollViewer and requires that you specify a change in the ScrollViewer template. ScrollContentPresenter needs to be wrapped inside the border with the name "PART_ScrollContentPresenterContainer".
Class FadingScrollViewer
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, XAML uses the default control with minimal changes for the default ScrollViewer template (this is 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> </local:FadingScrollViewer>
Check out these additional properties on the FadedScrollViewer: FadedEdgeThickness, FadedEdgeOpacity, and FadedEdgeFalloffSpeed
- FadedEdgeThickness: how much you want the attenuation to be (in pixels).
- FadedEdgeOpacity: how opaque the outer edge of the attenuation itself is. 0 = completely transparent at the edge, 1 = does not fade at all at the edge
- FadedEdgeFalloffSpeed: Determines how quickly the edge of the disappearing edge disappears when you approach it. The higher the value, the slower it disappears.