using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; using System.Windows.Media.Animation; namespace MediaBrowser.UI.Controls { /// /// This started from: /// http://www.switchonthecode.com/tutorials/wpf-tutorial-implementing-iscrollinfo /// Then, after implementing this, content was being displayed in stack panel like manner. /// I then reviewed the source code of ScrollContentPresenter and updated MeasureOverride and ArrangeOverride to match. /// public class ScrollingPanel : Grid, IScrollInfo { /// /// The infinite size /// private static Size InfiniteSize = new Size(double.PositiveInfinity, double.PositiveInfinity); /// /// The line size /// private const double LineSize = 16; /// /// The wheel size /// private const double WheelSize = 3 * LineSize; /// /// The _ offset /// private Vector _Offset; /// /// The _ extent /// private Size _Extent; /// /// The _ viewport /// private Size _Viewport; /// /// The _ animation length /// private TimeSpan _AnimationLength = TimeSpan.FromMilliseconds(125); /// /// When overridden in a derived class, measures the size in layout required for child elements and determines a size for the -derived class. /// /// The available size that this element can give to child elements. Infinity can be specified as a value to indicate that the element will size to whatever content is available. /// The size that this element determines it needs during layout, based on its calculations of child element sizes. protected override Size MeasureOverride(Size availableSize) { if (Children == null || Children.Count == 0) { return availableSize; } var constraint2 = availableSize; if (CanHorizontallyScroll) { constraint2.Width = double.PositiveInfinity; } if (CanVerticallyScroll) { constraint2.Height = double.PositiveInfinity; } var uiElement = Children[0]; uiElement.Measure(constraint2); var size = uiElement.DesiredSize; VerifyScrollData(availableSize, size); size.Width = Math.Min(availableSize.Width, size.Width); size.Height = Math.Min(availableSize.Height, size.Height); return size; } /// /// Arranges the content of a element. /// /// Specifies the size this element should use to arrange its child elements. /// that represents the arranged size of this Grid element and its children. protected override Size ArrangeOverride(Size arrangeSize) { this.VerifyScrollData(arrangeSize, _Extent); if (this.Children == null || this.Children.Count == 0) { return arrangeSize; } TranslateTransform trans = null; var uiElement = Children[0]; var finalRect = new Rect(uiElement.DesiredSize); // ScrollContentPresenter sets these to 0 - current offset // We need to set it to zero in order to make the animation work finalRect.X = 0; finalRect.Y = 0; finalRect.Width = Math.Max(finalRect.Width, arrangeSize.Width); finalRect.Height = Math.Max(finalRect.Height, arrangeSize.Height); trans = uiElement.RenderTransform as TranslateTransform; if (trans == null) { uiElement.RenderTransformOrigin = new Point(0, 0); trans = new TranslateTransform(); uiElement.RenderTransform = trans; } uiElement.Arrange(finalRect); trans.BeginAnimation(TranslateTransform.XProperty, GetAnimation(0 - HorizontalOffset), HandoffBehavior.Compose); trans.BeginAnimation(TranslateTransform.YProperty, GetAnimation(0 - VerticalOffset), HandoffBehavior.Compose); return arrangeSize; } /// /// Gets the animation. /// /// To value. /// DoubleAnimation. private DoubleAnimation GetAnimation(double toValue) { var animation = new DoubleAnimation(toValue, _AnimationLength); animation.EasingFunction = new ExponentialEase { EasingMode = EasingMode.EaseInOut }; return animation; } #region Movement Methods /// /// Scrolls down within content by one logical unit. /// public void LineDown() { SetVerticalOffset(VerticalOffset + LineSize); } /// /// Scrolls up within content by one logical unit. /// public void LineUp() { SetVerticalOffset(VerticalOffset - LineSize); } /// /// Scrolls left within content by one logical unit. /// public void LineLeft() { SetHorizontalOffset(HorizontalOffset - LineSize); } /// /// Scrolls right within content by one logical unit. /// public void LineRight() { SetHorizontalOffset(HorizontalOffset + LineSize); } /// /// Scrolls down within content after a user clicks the wheel button on a mouse. /// public void MouseWheelDown() { SetVerticalOffset(VerticalOffset + WheelSize); } /// /// Scrolls up within content after a user clicks the wheel button on a mouse. /// public void MouseWheelUp() { SetVerticalOffset(VerticalOffset - WheelSize); } /// /// Scrolls left within content after a user clicks the wheel button on a mouse. /// public void MouseWheelLeft() { SetHorizontalOffset(HorizontalOffset - WheelSize); } /// /// Scrolls right within content after a user clicks the wheel button on a mouse. /// public void MouseWheelRight() { SetHorizontalOffset(HorizontalOffset + WheelSize); } /// /// Scrolls down within content by one page. /// public void PageDown() { SetVerticalOffset(VerticalOffset + ViewportHeight); } /// /// Scrolls up within content by one page. /// public void PageUp() { SetVerticalOffset(VerticalOffset - ViewportHeight); } /// /// Scrolls left within content by one page. /// public void PageLeft() { SetHorizontalOffset(HorizontalOffset - ViewportWidth); } /// /// Scrolls right within content by one page. /// public void PageRight() { SetHorizontalOffset(HorizontalOffset + ViewportWidth); } #endregion /// /// Gets or sets a element that controls scrolling behavior. /// /// The scroll owner. /// A element that controls scrolling behavior. This property has no default value. public ScrollViewer ScrollOwner { get; set; } /// /// Gets or sets a value that indicates whether scrolling on the horizontal axis is possible. /// /// true if this instance can horizontally scroll; otherwise, false. /// true if scrolling is possible; otherwise, false. This property has no default value. public bool CanHorizontallyScroll { get; set; } /// /// Gets or sets a value that indicates whether scrolling on the vertical axis is possible. /// /// true if this instance can vertically scroll; otherwise, false. /// true if scrolling is possible; otherwise, false. This property has no default value. public bool CanVerticallyScroll { get; set; } /// /// Gets the vertical size of the extent. /// /// The height of the extent. /// A that represents, in device independent pixels, the vertical size of the extent.This property has no default value. public double ExtentHeight { get { return _Extent.Height; } } /// /// Gets the horizontal size of the extent. /// /// The width of the extent. /// A that represents, in device independent pixels, the horizontal size of the extent. This property has no default value. public double ExtentWidth { get { return _Extent.Width; } } /// /// Gets the horizontal offset of the scrolled content. /// /// The horizontal offset. /// A that represents, in device independent pixels, the horizontal offset. This property has no default value. public double HorizontalOffset { get { return _Offset.X; } } /// /// Gets the vertical offset of the scrolled content. /// /// The vertical offset. /// A that represents, in device independent pixels, the vertical offset of the scrolled content. Valid values are between zero and the minus the . This property has no default value. public double VerticalOffset { get { return _Offset.Y; } } /// /// Gets the vertical size of the viewport for this content. /// /// The height of the viewport. /// A that represents, in device independent pixels, the vertical size of the viewport for this content. This property has no default value. public double ViewportHeight { get { return _Viewport.Height; } } /// /// Gets the horizontal size of the viewport for this content. /// /// The width of the viewport. /// A that represents, in device independent pixels, the horizontal size of the viewport for this content. This property has no default value. public double ViewportWidth { get { return _Viewport.Width; } } /// /// Forces content to scroll until the coordinate space of a object is visible. /// /// A that becomes visible. /// A bounding rectangle that identifies the coordinate space to make visible. /// A that is visible. public Rect MakeVisible(Visual visual, Rect rectangle) { if (rectangle.IsEmpty || visual == null || visual == this || !base.IsAncestorOf(visual)) { return Rect.Empty; } rectangle = visual.TransformToAncestor(this).TransformBounds(rectangle); //rectangle.Inflate(50, 50); rectangle.Scale(1.2, 1.2); Rect viewRect = new Rect(HorizontalOffset, VerticalOffset, ViewportWidth, ViewportHeight); rectangle.X += viewRect.X; rectangle.Y += viewRect.Y; viewRect.X = CalculateNewScrollOffset(viewRect.Left, viewRect.Right, rectangle.Left, rectangle.Right); viewRect.Y = CalculateNewScrollOffset(viewRect.Top, viewRect.Bottom, rectangle.Top, rectangle.Bottom); SetHorizontalOffset(viewRect.X); SetVerticalOffset(viewRect.Y); rectangle.Intersect(viewRect); rectangle.X -= viewRect.X; rectangle.Y -= viewRect.Y; return rectangle; } /// /// Calculates the new scroll offset. /// /// The top view. /// The bottom view. /// The top child. /// The bottom child. /// System.Double. private static double CalculateNewScrollOffset(double topView, double bottomView, double topChild, double bottomChild) { bool offBottom = topChild < topView && bottomChild < bottomView; bool offTop = bottomChild > bottomView && topChild > topView; bool tooLarge = (bottomChild - topChild) > (bottomView - topView); if (!offBottom && !offTop) { return topView; } //Don't do anything, already in view if ((offBottom && !tooLarge) || (offTop && tooLarge)) { return topChild; } return (bottomChild - (bottomView - topView)); } /// /// Verifies the scroll data. /// /// The viewport. /// The extent. protected void VerifyScrollData(Size viewport, Size extent) { if (double.IsInfinity(viewport.Width)) { viewport.Width = extent.Width; } if (double.IsInfinity(viewport.Height)) { viewport.Height = extent.Height; } _Extent = extent; _Viewport = viewport; _Offset.X = Math.Max(0, Math.Min(_Offset.X, ExtentWidth - ViewportWidth)); _Offset.Y = Math.Max(0, Math.Min(_Offset.Y, ExtentHeight - ViewportHeight)); if (ScrollOwner != null) { ScrollOwner.InvalidateScrollInfo(); } } /// /// Sets the amount of horizontal offset. /// /// The degree to which content is horizontally offset from the containing viewport. public void SetHorizontalOffset(double offset) { offset = Math.Max(0, Math.Min(offset, ExtentWidth - ViewportWidth)); if (!offset.Equals(_Offset.X)) { _Offset.X = offset; InvalidateArrange(); } } /// /// Sets the amount of vertical offset. /// /// The degree to which content is vertically offset from the containing viewport. public void SetVerticalOffset(double offset) { offset = Math.Max(0, Math.Min(offset, ExtentHeight - ViewportHeight)); if (!offset.Equals(_Offset.Y)) { _Offset.Y = offset; InvalidateArrange(); } } } }