none
ScrollViewer, BringIntoView, Animation

    Question

  •  

    Hi,

    I have a Scrollviewer with a stackpanel with 5 expanders.

     

    When I click a button I want to bring expander 5 into view but smoothly. Just getting it to show up is easy enough to do with Exp5.BringIntoView() but I want it to slide into view instead of just abrubtly appearing. Can someone point to a tutorial for this, or point me in the right direction. I have no idea where to start or what property to animate.  

     

    Thanks in advance,

    Christian

    Monday, February 18, 2008 12:31 AM

Answers

  • This is not a matter of simply animating a property.  Unfortunately, there is no HorizontalOffset or VerticalOffset dependency property.  Just a readonly CLR property.

     

    To create a smoothly scrolling panel that can act as the items panel for your ItemsControl, you need to derive a custom panel and implement IScrollInfo on it.  Then you can control the positioning of the panel by implementing SetHorizontalOffset() and SetVerticalOffset().  You can begin an animation within these methods (perhaps leveraging a translate transform on the panel) to animate it into position.

     

    A proper IScrollInfo implementation is somewhat involved.  To get started, you might want to check out Ben Constable's 4-part series on implementing IScrollInfo (one, two, three, and four).

     

    Monday, February 18, 2008 1:02 AM

All replies

  • This is not a matter of simply animating a property.  Unfortunately, there is no HorizontalOffset or VerticalOffset dependency property.  Just a readonly CLR property.

     

    To create a smoothly scrolling panel that can act as the items panel for your ItemsControl, you need to derive a custom panel and implement IScrollInfo on it.  Then you can control the positioning of the panel by implementing SetHorizontalOffset() and SetVerticalOffset().  You can begin an animation within these methods (perhaps leveraging a translate transform on the panel) to animate it into position.

     

    A proper IScrollInfo implementation is somewhat involved.  To get started, you might want to check out Ben Constable's 4-part series on implementing IScrollInfo (one, two, three, and four).

     

    Monday, February 18, 2008 1:02 AM
  •  

    Thank you. I guess I will go ahead and submit a feature request to the WPF team then. As a Designer I can't imagine when you would not want smoothness when you call BringIntoView.

     

    Thanks

    Christian

    Monday, February 18, 2008 1:35 AM
  • ah, there's probably a simpler way of doing it without making your own control implementing IscrollInfo.  that's probably the 'best' way, but i just did a simpler way in a project a few weeks ago.

    on whatever event you want, say leftmouse down (in your case when clicking the header of your expander?)


    bool isAnimatingScroll = false;
    double begin = 0;
    double end = 0;
    double current = 0;
    bool isDown = false;

    public TitleObject selectedTitle;  // just a class, each title is data bound to each instance of expander
    int lastSelectedIndex = 0;

    public Window1()
    {
          InitializeComponent();
          CompositionTarget.Rendering += CompositionTarget_Rendering;
    }

    void CompositionTarget_Rendering(object sender, EventArgs e)
            {  
                if (isAnimatingScroll) animateScroll();
            }

    private void HeaderLeftMouseUp(object sender, MouseEventArgs e)
            {
                //Console.WriteLine("should start scroll now");
             
                // animate scroll
                begin = end = current = 0; // probably not necessary, just reset just in case
      int idx = this.catalog.titles.IndexOf(selectedTitle);  // titles is basically an observable collection

                if (idx > lastSelectedIndex) isDown = true; // moving down in listbox
                else isDown = false; // moving up

                begin = current = this.scrollViewer.VerticalOffset;  // init these values to same

                int i = 0;
                double height = 0;

                while (i < idx)
                {
                    TitleObject title = (TitleObject)this.titlesLB.Items[ i ];  // titlesLB is my ListBox filled with Expanders
                    ContentPresenter cp = (ContentPresenter)this.titlesLB.ItemContainerGenerator.ContainerFromItem(title);
                    height += cp.ActualHeight;
                    //Console.WriteLine("lbi.height: " + lbi.ActualHeight);               
                    i++;
                }

                //Console.WriteLine("total: " + height);
               
               
                if (idx > 1)
                    end = height;
                isAnimatingScroll = true;
                lastSelectedIndex = idx;
               
            }

    void animateScroll() // function to animate scrollviewer
            {
                if (isDown)
                {
                    current += (end - begin) / 20;
                    if (current >= end)
                    {
                        isAnimatingScroll = false;
                        current = end;
                    }
                }
                else
                {
                    current += (end - begin) / 20;

                    if (current <= end)
                    {
                        isAnimatingScroll = false;
                        current = end;
                    }
                }
              
                this.scrollViewer.ScrollToVerticalOffset(current);
            }

    of course, this is just bits to show you another way, not all of the code... maybe it helps?   tell me if you need something more to go on...

    bigshiny90
    Monday, February 18, 2008 4:40 AM
  •  bigshiny90 wrote:

    ah, there's probably a simpler way of doing it...

     

    I would never want to discourage a good hack... Wink

     

    ...so I'll just warn you that with this method, your code is in contention with the ScrollViewer.  With the IScrollInfo approach, on the other hand, your code works in conjunction with the ScrollViewer.

     

    The biggest drawback to using the rendering event to update the ScrollViewer's offset is that the user may be interactively doing something that tries to simultaneously affect the offset (such as keyboard navigation in the listbox, grabbing the scroll thumb to scrub through items, clicking the LineUp/Down or PageUp/Down buttons, etc).  This could create a very confusing user experience.

     

    On the other hand, if you can get your desired behavior for your particular use case, it's certainly a bit easier than implementing your own panel. 

     

     

     

    Monday, February 18, 2008 12:47 PM
  • agreed... probably not the best hack out there, but so far workable for the short term. (haven't had any major difficulties at least!)  i've been thinking the last few weeks, of implementing the IScrollInfo instead, feeling it was a best solution.. just a matter of time!

    if i do implement it, i'll post it - if anyone finds it useful.

    the other problem with the rendering event, is the speed is rather dependent on the host machine also, so doesn't give a consistent experience.

    the idea was for a quick hack not a permanent solution!  thought it might be usefull to the original poster if they were discouraged from continuing with IScrollInfo...


    Monday, February 18, 2008 6:40 PM
  • Thansk a bunch, both of you. I would much appreciate it if you posted the code for a IScrollViewer. I don't think I can get my dev team to adopt the BigShiny90's solution but I also can't get them to write a new control given all the other work we have on our plate. I am currently working on other ways of communicate the transistion but none are working very well so far. I.e. I am animating the Expander on RequestBringIntoView. Anyway. Thanks for the help.  And more help is even more welcome Smile
    • Proposed as answer by Olaf Rabbachin Friday, January 29, 2010 6:55 PM
    Tuesday, February 19, 2008 2:59 AM
  • Though this might fall into the category of a hack, the easiest thing I've come up with is to extend the scrollviewer class and add two dependent properties. When these properties are changed I update the base vertical and horizontal offset of the scrollviewer. In order to use the animation code you'll need the .net 3.5 sp1 framework:

    public class AniScrollViewer:ScrollViewer
    {

    public static DependencyProperty CurrentVerticalOffsetProperty = DependencyProperty.Register("CurrentVerticalOffset", typeof(double), typeof(AniScrollViewer), new PropertyMetadata(new PropertyChangedCallback(OnVerticalChanged)));

    public static DependencyProperty CurrentHorizontalOffsetProperty = DependencyProperty.Register("CurrentHorizontalOffsetOffset", typeof(double), typeof(AniScrollViewer), new PropertyMetadata(new PropertyChangedCallback(OnHorizontalChanged)));

    private static void OnVerticalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
    AniScrollViewer viewer = d as AniScrollViewer;
    viewer.ScrollToVerticalOffset((
    double)e.NewValue);
    }

    private static void OnHorizontalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
    AniScrollViewer viewer = d as AniScrollViewer;
    viewer.ScrollToHorizontalOffset((
    double)e.NewValue);
    }

    public double CurrentHorizontalOffset
    {
    get { return (double)this.GetValue(CurrentHorizontalOffsetProperty); }
    set { this.SetValue(CurrentHorizontalOffsetProperty, value); }
    }

    public double CurrentVerticalOffset
    {
    get { return (double)this.GetValue(CurrentVerticalOffsetProperty); }
    set { this.SetValue(CurrentVerticalOffsetProperty, value); }
    }
    }

    Here is an example of the animation code I use:

    private void ScrollToPosition(double x, double y)
    {
    DoubleAnimation vertAnim = new DoubleAnimation();
    vertAnim.From = MainScrollViewer.VerticalOffset;
    vertAnim.To = y;
    vertAnim.DecelerationRatio = .2;
    vertAnim.Duration =
    new Duration(TimeSpan.FromMilliseconds(250));

    DoubleAnimation horzAnim = new DoubleAnimation();
    horzAnim.From = MainScrollViewer.HorizontalOffset;
    horzAnim.To = x;
    horzAnim.DecelerationRatio = .2;
    horzAnim.Duration =
    new Duration(TimeSpan.FromMilliseconds(300));

    Storyboard sb = new Storyboard();
    sb.Children.Add(vertAnim);
    sb.Children.Add(horzAnim);

    Storyboard.SetTarget(vertAnim, MainScrollViewer);
    Storyboard.SetTargetProperty(vertAnim, new PropertyPath(AniScrollViewer.CurrentVerticalOffsetProperty));
    Storyboard.SetTarget(horzAnim, MainScrollViewer);
    Storyboard.SetTargetProperty(horzAnim, new PropertyPath(AniScrollViewer.CurrentHorizontalOffsetProperty));

    sb.Begin();

    }


    Hope this helps others....

    • Proposed as answer by Dr. YSG Monday, February 02, 2009 2:09 AM
    Sunday, February 01, 2009 9:56 PM
  • I just had this brainstorm to do things this way last Friday. Did some research today, and decided tonight to see if anyone else came up with it. My goodness, you beat me my 4 hours! I love it.




    Dr. YSG
    Monday, February 02, 2009 2:09 AM
  • Hi Onixfire
    How do you have to add your AniScrollViewer to the XAML file in a proyect?
    or you have to create a custom control?
    Do you have a sample?

    thanks

    Wally
    Walther
    • Proposed as answer by Onyxfire Wednesday, March 25, 2009 4:07 AM
    Monday, March 23, 2009 6:49 AM
  • I created a simple Codeplex project using this technique.  You can get the source code and check it out.  Hope this helps.
     
    Here is the URL:
    http://aniscrollviewer.codeplex.com/
    • Proposed as answer by Onyxfire Wednesday, March 25, 2009 4:10 AM
    Wednesday, March 25, 2009 4:10 AM
  • Good Post. The function ScrollToPosition can be a method of the ScrollViewer derived class (AniScrollViewer). Makes it a little more 'air-tight'. Thanks. --- Ahsan
    Wednesday, July 29, 2009 9:49 PM
  • But how to use the named controls as children of this Scrollviewer?

    Because

     <AniScrollViewer x:Name="Scroll_Ctrl">
                  
       <Viewbox x:Name="My_Viewbox" Stretch="None">
    
    </AniScrollViewer

    is going to give error message like "Cannot set Name attribute value 'AniScrollViewer' on element... Blah Blah".


    Thursday, February 09, 2012 10:44 AM
  • Got Solution ... The problem is with the XAML file. Whenever you create user control, it creates XAML and .cs file and this style does not allow you to create named controls. Instead use the code to build the control and remove the xaml file. Add this in  AniScrollViewer and it should work fine. In this way we can add the named controls inside the custom control.

     
    protected override void OnInitialized(EventArgs e)
            {
                base.OnInitialized(e);
    
                ContentPresenter content = new ContentPresenter();
                content.Content = Content;
                Grid grid = new Grid();
                grid.Children.Add(content);            
                Content = grid;
            }

    Mark this if it is useful..

    Thursday, February 09, 2012 10:56 AM
  • thanks for sharing:).. it worked very:)
    Sunday, September 16, 2012 3:05 AM