Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scrollviewer edge blur effect, opacitymask not working properly

I want to create a custom scrollviewer control for a touch screen application without using scrollbars. To accomplish letting the users know that they can scroll the content, I am fading the bottom and top part of the scrollviewer with a linear gradient using an opacitymask. This all works fine, except for a problem with the opacitymask applying to the textblock in addition to the scrollviewer!

What I mean is, I would like the fading effect to apply to the top 1% and bottom 1% of the scrollviewer, and then the middle of the scrollviewer will be visible. The problem, however, is that this effect is also happening on the control within the scrollviewer as well, even if i set OpacityMask="{x:Null}" on the textblock.

I have tried applying the opacitymask to the outside of the scrollviewer as well but the same problem happens. Does the Opacitymask property apply to the children as well? Is there a better way to doing this fading effect?

Here is the code I am 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>
like image 209
Willson Haw Avatar asked Dec 14 '09 15:12

Willson Haw


1 Answers

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:

  1. Fades the edges of the content within the ScrollViewer if there is content along that side of the ScrollViewer that is not currently visible.

  2. Decreases the intensity of the fade effect as you scroll closer to the edge of the content.

  3. Gives some control over how the faded edges look. Specifically, you can control:

    1. Thickness of the faded edge
    2. How opaque the content is at the outermost edge (or how "intense" the fade is)
    3. 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.
like image 126
Nathan Avatar answered Oct 01 '22 04:10

Nathan