The effect of blurring the edge of the scroll, opacitymask is not working properly

I want to create my own scrollviewer control for a touchscreen application without using scroll bars. To tell users that they can scroll content, I fade out the bottom and top of the scrollviewer using a linear gradient using opacitymask. All this works great, except for the opacitymask problem applied to the text block in addition to the scrollviewer!

What I mean, I would like the fading effect to apply to the top 1% and bottom 1% of the scrollviewer, and then the middle part of the scrollviewer will be visible. The problem, however, is that this effect also occurs in the control in the scrollviewer, even if I set OpacityMask = "{x: Null}" in the text block.

I tried applying opacitymask to the outside of the scrollviewer, but the same problem occurs. Does the Opacitymask apply to children? Is there a better way to make this fade effect?

Here is the code I'm using:

<Grid Width="200" Height="130"> <ScrollViewer BorderThickness="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="2" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Hidden" > <ScrollViewer.OpacityMask> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="Transparent" Offset="0" /> <GradientStop Color="Black" Offset="0.1" /> <GradientStop Color="Black" Offset="0.9" /> <GradientStop Color="Transparent" Offset="1" /> </LinearGradientBrush> </ScrollViewer.OpacityMask> <TextBlock Margin="0,10" Style="{StaticResource textSmall}" TextWrapping="Wrap"> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum </TextBlock> </ScrollViewer> </Grid> 
+4
source share
5 answers

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> <!-- Your content here --> </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.
+6
source

I changed the Grandpappy code to work without using a custom control. Similarly, you can “refresh” all of your ScrollViewers before fading scrollviewers by simply adding an attached property to them. You also need to specify a style for the scrollviewer, which I did anyway. It must define the name PART_ScrollContentPresenterContainer. You can use the Grandpappys style unchanged.

Code for the class (called ScrollViewerExtensions):

 /// <summary> /// Attached property that makes the scrollbar fade on it edges /// /// derived class at from http://stackoverflow.com/questions/1901709/scrollviewer-edge-blur-effect-opacitymask-not-working-properly /// </summary> public class ScrollViewerExtensions : DependencyObject { /// <summary> /// MAIN property: this activates the whole fading effect /// </summary> public static readonly DependencyProperty FadedEdgeThicknessProperty = DependencyProperty.RegisterAttached("FadedEdgeThickness", typeof(double), typeof(ScrollViewerExtensions), new PropertyMetadata(20.0d, OnFadedEdgeThicknessChanged)); public static void SetFadedEdgeThickness(ScrollViewer s, double value) { s.SetValue(FadedEdgeThicknessProperty, value); } public static double GetFadedEdgeThickness(ScrollViewer s, double value) { return (double)s.GetValue(FadedEdgeThicknessProperty); } /// <summary> /// optional property. changes how fast the fade appears/diappears when scrolling near an edge /// </summary> public static readonly DependencyProperty FadedEdgeFalloffSpeedProperty = DependencyProperty.RegisterAttached("FadedEdgeFalloffSpeed", typeof(double), typeof(ScrollViewerExtensions), new PropertyMetadata(4.0d, OnFadedEdgeFalloffSpeedChanged)); public static void SetFadedEdgeFalloffSpeed(ScrollViewer s, double value) { s.SetValue(FadedEdgeFalloffSpeedProperty, value); } public static double GetFadedEdgeFalloffSpeed(ScrollViewer s, double value) { return (double)s.GetValue(FadedEdgeFalloffSpeedProperty); } /// <summary> /// optional property. changes how opaque the outermost edge should be /// </summary> public static readonly DependencyProperty FadedEdgeOpacityProperty = DependencyProperty.RegisterAttached("FadedEdgeOpacity", typeof(double), typeof(ScrollViewerExtensions), new PropertyMetadata(0.0d, OnFadedEdgeOpacityChanged)); public static void SetFadedEdgeOpacity(ScrollViewer s, double value) { s.SetValue(FadedEdgeOpacityProperty, value); } public static double GetFadedEdgeOpacity(ScrollViewer s, double value) { return (double)s.GetValue(FadedEdgeOpacityProperty); } private const string PART_SCROLL_PRESENTER_CONTAINER_NAME = "PART_ScrollContentPresenterContainer"; private static Dictionary<ScrollViewer, FadeSettings> Settings = new Dictionary<ScrollViewer, FadeSettings>(); /// <summary> /// this is kindof the constructor for the properties. If you don't specify this, nothing will fade! /// </summary> /// <param name="d"></param> /// <param name="e"></param> public static void OnFadedEdgeThicknessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var scrollViewer = d as ScrollViewer; if (scrollViewer == null) return; double edgeThickness = (double)e.NewValue; scrollViewer.ScrollChanged += FadingScrollViewer_ScrollChanged; scrollViewer.SizeChanged += FadingScrollViewer_SizeChanged; if (!Settings.ContainsKey(scrollViewer)) Settings.Add(scrollViewer, new FadeSettings()); Settings[scrollViewer].FadedEdgeThickness = edgeThickness; } public static void OnFadedEdgeFalloffSpeedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var scrollViewer = d as ScrollViewer; if (scrollViewer == null) return; double edgeFalloffSpeed = (double)e.NewValue; if (!Settings.ContainsKey(scrollViewer)) Settings.Add(scrollViewer, new FadeSettings()); Settings[scrollViewer].FadedEdgeFalloffSpeed = edgeFalloffSpeed; } public static void OnFadedEdgeOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var scrollViewer = d as ScrollViewer; if (scrollViewer == null) return; double edgeOpacity = (double)e.NewValue; if (!Settings.ContainsKey(scrollViewer)) Settings.Add(scrollViewer, new FadeSettings()); Settings[scrollViewer].FadedEdgeOpacity = edgeOpacity; } private static void FadingScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) { ScrollViewer scrollViewer = sender as ScrollViewer; FadeSettings settings = Settings[scrollViewer]; if (settings.InnerFadedBorder == null) return; var topOffset = CalculateNewMarginBasedOnOffsetFromEdge(scrollViewer, scrollViewer.VerticalOffset); ; var bottomOffset = CalculateNewMarginBasedOnOffsetFromEdge(scrollViewer, scrollViewer.ScrollableHeight - scrollViewer.VerticalOffset); var leftOffset = CalculateNewMarginBasedOnOffsetFromEdge(scrollViewer, scrollViewer.HorizontalOffset); var rightOffset = CalculateNewMarginBasedOnOffsetFromEdge(scrollViewer, scrollViewer.ScrollableWidth - scrollViewer.HorizontalOffset); settings.InnerFadedBorder.Margin = new Thickness(leftOffset, topOffset, rightOffset, bottomOffset); } private static void FadingScrollViewer_SizeChanged(object sender, SizeChangedEventArgs e) { ScrollViewer scrollViewer = sender as ScrollViewer; FadeSettings settings = Settings[scrollViewer]; if (!settings.Initialized) // abuse the SizeChanged event to call the OnApplyTemplate method. We can't override it, so we need something, that fires after it would normally be called. see http://msdn.microsoft.com/en-us/library/dd351483%28v=vs.95%29.aspx { OnApplyTemplate(scrollViewer); settings.Initialized = true; } if (settings.OuterFadedBorder == null || settings.InnerFadedBorder == null || settings.InnerFadedBorderEffect == null) return; settings.OuterFadedBorder.Width = e.NewSize.Width; settings.OuterFadedBorder.Height = e.NewSize.Height; double innerFadedBorderBaseMarginThickness = settings.FadedEdgeThickness / 2.0; settings.InnerFadedBorder.Margin = new Thickness(innerFadedBorderBaseMarginThickness); settings.InnerFadedBorderEffect.Radius = settings.FadedEdgeThickness; } private static double CalculateNewMarginBasedOnOffsetFromEdge(ScrollViewer scrollViewer, double edgeOffset) { FadeSettings settings = Settings[scrollViewer]; var innerFadedBorderBaseMarginThickness = settings.FadedEdgeThickness / 2.0; //var calculatedOffset = (innerFadedBorderBaseMarginThickness) - (1.0 * (this.FadedEdgeThickness - (edgeOffset / this.FadedEdgeFalloffSpeed))); double calculatedOffset; if (edgeOffset == 0) calculatedOffset = -innerFadedBorderBaseMarginThickness; else calculatedOffset = (edgeOffset * settings.FadedEdgeFalloffSpeed) - innerFadedBorderBaseMarginThickness; return Math.Min(innerFadedBorderBaseMarginThickness, calculatedOffset); } public static void OnApplyTemplate(ScrollViewer scrollViewer) { BuildInnerFadedBorderEffectForOpacityMask(scrollViewer); BuildInnerFadedBorderForOpacityMask(scrollViewer); BuildOuterFadedBorderForOpacityMask(scrollViewer); SetOpacityMaskOfScrollContainer(scrollViewer); } private static void BuildInnerFadedBorderEffectForOpacityMask(ScrollViewer scrollViewer) { FadeSettings settings = Settings[scrollViewer]; settings.InnerFadedBorderEffect = new BlurEffect() { RenderingBias = RenderingBias.Performance, }; } private static void BuildInnerFadedBorderForOpacityMask(ScrollViewer scrollViewer) { FadeSettings settings = Settings[scrollViewer]; settings.InnerFadedBorder = new Border() { Background = Brushes.Black, HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, VerticalAlignment = System.Windows.VerticalAlignment.Stretch, Effect = settings.InnerFadedBorderEffect, }; } private static void BuildOuterFadedBorderForOpacityMask(ScrollViewer scrollViewer) { FadeSettings settings = Settings[scrollViewer]; byte fadedEdgeByteOpacity = (byte)(settings.FadedEdgeOpacity * 255); settings.OuterFadedBorder = new Border() { Background = new SolidColorBrush(Color.FromArgb(fadedEdgeByteOpacity, 0, 0, 0)), ClipToBounds = true, Child = settings.InnerFadedBorder, }; } private static void SetOpacityMaskOfScrollContainer(ScrollViewer scrollViewer) { FadeSettings settings = Settings[scrollViewer]; var opacityMaskBrush = new VisualBrush() { Visual = settings.OuterFadedBorder }; var scrollContentPresentationContainer = scrollViewer.Template.FindName(PART_SCROLL_PRESENTER_CONTAINER_NAME, scrollViewer) as UIElement; if (scrollContentPresentationContainer == null) return; scrollContentPresentationContainer.OpacityMask = opacityMaskBrush; // test /*var container = scrollContentPresentationContainer as Border; var scroller = container.Child as UIElement; container.Child = null; Grid g = new Grid(); container.Child = g; g.Children.Add(scroller); this.OuterFadedBorder.IsHitTestVisible = false; g.Children.Add(this.OuterFadedBorder);*/ } protected class FadeSettings { public BlurEffect InnerFadedBorderEffect { get; set; } public Border InnerFadedBorder { get; set; } public Border OuterFadedBorder { get; set; } public double FadedEdgeThickness { get; set; } public double FadedEdgeFalloffSpeed { get; set; } public double FadedEdgeOpacity { get; set; } public bool Initialized { get; set; } public FadeSettings() { FadedEdgeThickness = 20.0d; FadedEdgeFalloffSpeed = 4.0d; FadedEdgeOpacity = 0.0d; } } } 

You can either set a nested property in the code:

 scroller.SetValue(ScrollViewerExtensions.FadedEdgeThicknessProperty, 70.0d); 

or you define it directly in XAML:

 <ScrollViewer Name="scrollViewer" controls:ScrollViewerExtensions.FadedEdgeThickness="40"> some content </ScrollViewer> 

Hope you guys can do something about it!

+3
source

You can add a transparent gradient control on top of the ScrollViewer within the same grid and set its IsHitTestVisible to false to achieve this effect. The following is a modified version of your example using the Canvas control on top of ScrollViewer:

 <Grid Width="200" Height="130"> <ScrollViewer BorderThickness="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="2" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Hidden" Background="LightBlue"> <TextBlock Margin="10,30,10,30" TextWrapping="Wrap" OpacityMask="Black" VerticalAlignment="Center"> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum </TextBlock> </ScrollViewer> <Canvas Width="200" Height="130" Focusable="False" IsEnabled="False" IsHitTestVisible="False"> <Canvas.Background> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="#FFFFFFFF" Offset="0" /> <GradientStop Color="#00FFFFFF" Offset="0.1" /> <GradientStop Color="#00FFFFFF" Offset="0.9" /> <GradientStop Color="#FFFFFFFF" Offset="1" /> </LinearGradientBrush> </Canvas.Background> </Canvas> </Grid> 

I added the background color to ScrollViewer and the large top edge of the TextBlock so that it is easy to confirm the desired effect. Here is the result: alt text

+1
source

Today I faced the same problem, and the solution is actually very simple: set the ScrollViewer.Background property to anything but zero (in your case you want Transparent), and it works.

I am posting an answer here, as this is the only question I have found regarding this issue.

+1
source

Antoine is right, except that he has a downside. You need to set the background color in the text block to white or transparent or whatever you want.

  <Grid Background="LightGray"> <ScrollViewer BorderThickness="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="2" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Hidden" > <ScrollViewer.OpacityMask> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="Transparent" Offset="0" /> <GradientStop Color="Black" Offset="0.1" /> <GradientStop Color="Black" Offset="0.9" /> <GradientStop Color="Transparent" Offset="1" /> </LinearGradientBrush> </ScrollViewer.OpacityMask> <TextBlock Margin="0,10" TextWrapping="Wrap" Width="200" Background="White"> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum </TextBlock> </ScrollViewer> </Grid> 

If you delete the TextBlock background, you will notice that the text in the text block will disappear, but given the background, it will only apply it to the scrollviewer.

+1
source

All Articles