diff --git a/DFGUI/Scripts/Controls/dfScrollPanel.cs b/DFGUI/Scripts/Controls/dfScrollPanel.cs index bf0d43c..34d5158 100644 --- a/DFGUI/Scripts/Controls/dfScrollPanel.cs +++ b/DFGUI/Scripts/Controls/dfScrollPanel.cs @@ -1,4 +1,4 @@ -/* Copyright 2013 Daikon Forge */ +/* Copyright 2013 Daikon Forge */ using System; using System.Linq; @@ -11,472 +11,474 @@ /// /// Implements a scrollable control container /// -[dfCategory( "Basic Controls" )] -[dfTooltip( "Implements a scrollable control container" )] -[dfHelp( "http://www.daikonforge.com/docs/df-gui/classdf_scroll_panel.html" )] +[dfCategory("Basic Controls")] +[dfTooltip("Implements a scrollable control container")] +[dfHelp("http://www.daikonforge.com/docs/df-gui/classdf_scroll_panel.html")] [Serializable] [ExecuteInEditMode] -[AddComponentMenu( "Daikon Forge/User Interface/Containers/Scrollable Panel" )] +[AddComponentMenu("Daikon Forge/User Interface/Containers/Scrollable Panel")] public class dfScrollPanel : dfControl { - #region Public events + #region Public events - /// - /// Raised when the value of the property has changed - /// - public event PropertyChangedEventHandler ScrollPositionChanged; + /// + /// Raised when the value of the property has changed + /// + public event PropertyChangedEventHandler ScrollPositionChanged; - #endregion + #endregion - #region Enumerations + #region Enumerations - /// - /// Specifies the direction to arrange controls when flow layout is used - /// - public enum LayoutDirection : int - { - /// - /// Controls will be arranged horizontally - /// - Horizontal, - /// - /// Controls will be arranged vertically - /// - Vertical - } + /// + /// Specifies the direction to arrange controls when flow layout is used + /// + public enum LayoutDirection : int + { + /// + /// Controls will be arranged horizontally + /// + Horizontal, + /// + /// Controls will be arranged vertically + /// + Vertical + } - #endregion + #endregion - #region Serialized protected members + #region Serialized protected members - [SerializeField] - protected dfAtlas atlas; + [SerializeField] + protected dfAtlas atlas; - [SerializeField] - protected string backgroundSprite; + [SerializeField] + protected string backgroundSprite; - [SerializeField] - protected Color32 backgroundColor = UnityEngine.Color.white; + [SerializeField] + protected Color32 backgroundColor = UnityEngine.Color.white; - [SerializeField] - protected bool autoReset = true; + [SerializeField] + protected bool autoReset = true; - [SerializeField] - protected bool autoLayout = false; + [SerializeField] + protected bool autoLayout = false; - [SerializeField] - protected RectOffset scrollPadding = new RectOffset(); - - [SerializeField] - protected RectOffset flowPadding = new RectOffset(); - - [SerializeField] - protected LayoutDirection flowDirection = LayoutDirection.Horizontal; - - [SerializeField] - protected bool wrapLayout = false; - - [SerializeField] - protected Vector2 scrollPosition = Vector2.zero; - - [SerializeField] - protected int scrollWheelAmount = 10; - - [SerializeField] - protected dfScrollbar horzScroll; - - [SerializeField] - protected dfScrollbar vertScroll; - - [SerializeField] - protected dfControlOrientation wheelDirection = dfControlOrientation.Horizontal; - - [SerializeField] - protected bool scrollWithArrowKeys = false; - - [SerializeField] - protected bool useScrollMomentum = false; - - [SerializeField] - protected bool useVirtualScrolling = false; - - [SerializeField] - protected bool autoFitVirtualTiles = true; - - [SerializeField] - protected dfControl virtualScrollingTile; - - #endregion - - #region Private instance variables - - private bool initialized = false; - private bool resetNeeded = false; - private bool scrolling = false; - private bool isMouseDown = false; - private Vector2 touchStartPosition = Vector2.zero; - private Vector2 scrollMomentum = Vector2.zero; - - /// - /// Contains information about virtual data stored for this virtualized dfScrollPanel. - /// Must be cast to , where T is the Type in the provided as the backing list - /// in order to be of any use. - /// - private object virtualScrollData; - - #endregion - - #region Public properties - - /// - /// Gets or sets whether scrolling with the mousewheel or touch swipe will - /// add a momentum effect to scrolling - /// - public bool UseScrollMomentum - { - get { return this.useScrollMomentum; } - set { this.useScrollMomentum = value; scrollMomentum = Vector2.zero; } - } - - /// - /// Set to TRUE if you want the to scroll when the - /// user presses the arrow keys. - /// - public bool ScrollWithArrowKeys - { - get { return scrollWithArrowKeys; } - set { scrollWithArrowKeys = value; } - } - - /// - /// The Texture Atlas containing the images used by this control - /// - public dfAtlas Atlas - { - get - { - if( this.atlas == null ) - { - var view = GetManager(); - if( view != null ) - { - return this.atlas = view.DefaultAtlas; - } - } - return this.atlas; - } - set - { - if( !dfAtlas.Equals( value, atlas ) ) - { - this.atlas = value; - Invalidate(); - } - } - } - - /// - /// The name of the image in the that will be used to - /// render the background of this control - /// - public string BackgroundSprite - { - get { return backgroundSprite; } - set - { - if( value != backgroundSprite ) - { - backgroundSprite = value; - Invalidate(); - } - } - } - - /// - /// Gets or set the color that will be applied to the background sprite - /// - public Color32 BackgroundColor - { - get { return backgroundColor; } - set - { - if( !Color32.Equals( value, backgroundColor ) ) - { - backgroundColor = value; - Invalidate(); - } - } - } - - /// - /// Gets or sets whether the will automatically - /// reset the scrolling region - /// - public bool AutoReset - { - get { return this.autoReset; } - set - { - if( value != this.autoReset ) - { - this.autoReset = value; - Reset(); - } - } - } - - /// - /// Gets or sets the amount of padding that will be applied when arranging - /// child controls if the property is set to TRUE - /// - public RectOffset ScrollPadding - { - get - { - if( this.scrollPadding == null ) - this.scrollPadding = new RectOffset(); - return this.scrollPadding; - } - set - { - value = value.ConstrainPadding(); - if( !RectOffset.Equals( value, this.scrollPadding ) ) - { - this.scrollPadding = value; - if( AutoReset || AutoLayout ) - Reset(); - } - } - } - - /// - /// Gets or sets whether child controls will be automatically arranged - /// - public bool AutoLayout - { - get { return this.autoLayout; } - set - { - if( value != this.autoLayout ) - { - this.autoLayout = value; - if( AutoReset || AutoLayout ) - Reset(); - } - } - } - - /// - /// Gets or sets whether controls that lie outside of this container's - /// boundaries will be "wrapped" to the next row or column when using AutoLayout - /// - public bool WrapLayout - { - get { return this.wrapLayout; } - set - { - if( value != this.wrapLayout ) - { - this.wrapLayout = value; - Reset(); - } - } - } - - /// - /// Gets or sets the direction in which child controls will be arranged - /// when using AutoLayout - /// - public LayoutDirection FlowDirection - { - get { return this.flowDirection; } - set - { - if( value != this.flowDirection ) - { - this.flowDirection = value; - Reset(); - } - } - } - - /// - /// Gets or sets the amount of padding that will be applied to each control - /// when arranging child controls using AutoLayout - /// - public RectOffset FlowPadding - { - get - { - if( this.flowPadding == null ) - this.flowPadding = new RectOffset(); - return this.flowPadding; - } - set - { - value = value.ConstrainPadding(); - if( !RectOffset.Equals( value, this.flowPadding ) ) - { - this.flowPadding = value; - Reset(); - } - } - } - - /// - /// Gets or sets the upper-left position of the viewport relative - /// to the entire scrollable area - /// - public Vector2 ScrollPosition - { - get { return this.scrollPosition; } - set - { - - var viewSize = calculateViewSize(); - var clientSize = new Vector2( this.size.x - this.scrollPadding.horizontal, this.size.y - this.scrollPadding.vertical ); - - value = Vector2.Min( viewSize - clientSize, value ); - value = Vector2.Max( Vector2.zero, value ); - value = value.RoundToInt(); - - if( ( value - this.scrollPosition ).sqrMagnitude > float.Epsilon ) - { - - var delta = value - scrollPosition; - this.scrollPosition = value; - - scrollChildControls( delta ); - updateScrollbars(); - - } - - OnScrollPositionChanged(); - - } - } - - /// - /// Gets or sets the distance in pixels that the scroll panel will be scrolled when - /// the user rotates the mouse wheel (this value is overridden by scrollbars, if they - /// are attached) - /// - public int ScrollWheelAmount - { - get { return this.scrollWheelAmount; } - set { this.scrollWheelAmount = value; } - } - - /// - /// Gets or sets a reference the the instance - /// that is used to scroll this container horizontally - /// - public dfScrollbar HorzScrollbar - { - get { return this.horzScroll; } - set - { - horzScroll = value; - updateScrollbars(); - } - } - - /// - /// Gets or sets a reference the the instance - /// that is used to scroll this container vertically - /// - public dfScrollbar VertScrollbar - { - get { return this.vertScroll; } - set - { - vertScroll = value; - updateScrollbars(); - } - } - - /// - /// Indicates the direction to scroll when the user scrolls the mouse wheel - /// - public dfControlOrientation WheelScrollDirection - { - get { return this.wheelDirection; } - set { this.wheelDirection = value; } - } - - /// - /// Gets or sets whether or not the will use a virtual scrolling - /// algorithm for recycling control tiles. - /// - public bool UseVirtualScrolling - { - get { return useVirtualScrolling; } - set - { - useVirtualScrolling = value; - - if ( !value ) - { - VirtualScrollingTile = null; - } - } - } - - /// - /// Gets or sets whether or not virtualized tiles will be automatically stretched to fit horizontally for vertically - /// scrolling , or vertically for horizontally scrolling controls. - /// - public bool AutoFitVirtualTiles - { - get { return autoFitVirtualTiles; } - set { autoFitVirtualTiles = value; } - } - - /// - /// The to be copied and recycled during virtual scrolling. - /// Attached to this control must be a inherited script that also implements - /// - public dfControl VirtualScrollingTile - { - get { return ( useVirtualScrolling ) ? virtualScrollingTile : null; } - set { virtualScrollingTile = ( useVirtualScrolling ) ? value : null; } - } - - #endregion - - #region Overrides - - /// - /// Returns the padding used when clipping is enabled and - /// the renderer is using shader-based clipping - /// - /// - protected internal override RectOffset GetClipPadding() - { - return this.scrollPadding ?? dfRectOffsetExtensions.Empty; - } - - protected internal override Plane[] GetClippingPlanes() - { - - if( !ClipChildren ) - return null; - - var corners = GetCorners(); - - var right = transform.TransformDirection( Vector3.right ); - var left = transform.TransformDirection( Vector3.left ); - var up = transform.TransformDirection( Vector3.up ); - var down = transform.TransformDirection( Vector3.down ); - - var p2u = PixelsToUnits(); - var padding = ScrollPadding; - corners[ 0 ] += right * padding.left * p2u + down * padding.top * p2u; - corners[ 1 ] += left * padding.right * p2u + down * padding.top * p2u; - corners[ 2 ] += right * padding.left * p2u + up * padding.bottom * p2u; - - return new Plane[] + [SerializeField] + protected RectOffset scrollPadding = new RectOffset(); + + [SerializeField] + protected RectOffset flowPadding = new RectOffset(); + + [SerializeField] + protected LayoutDirection flowDirection = LayoutDirection.Horizontal; + + [SerializeField] + protected bool wrapLayout = false; + + [SerializeField] + protected Vector2 scrollPosition = Vector2.zero; + + [SerializeField] + protected int scrollWheelAmount = 10; + + [SerializeField] + protected dfScrollbar horzScroll; + + [SerializeField] + protected dfScrollbar vertScroll; + + [SerializeField] + protected dfControlOrientation wheelDirection = dfControlOrientation.Horizontal; + + [SerializeField] + protected bool scrollWithArrowKeys = false; + + [SerializeField] + protected bool useScrollMomentum = false; + + [SerializeField] + protected bool useVirtualScrolling = false; + + [SerializeField] + protected bool autoFitVirtualTiles = true; + + [SerializeField] + protected dfControl virtualScrollingTile; + + #endregion + + #region Private instance variables + + private bool initialized = false; + private bool resetNeeded = false; + private bool scrolling = false; + private bool isMouseDown = false; + private Vector2 touchStartPosition = Vector2.zero; + private Vector2 scrollMomentum = Vector2.zero; + + /// + /// Contains information about virtual data stored for this virtualized dfScrollPanel. + /// Must be cast to , where T is the Type in the provided as the backing list + /// in order to be of any use. + /// + private object virtualScrollData; + + private float lastWidth=0; + + #endregion + + #region Public properties + + /// + /// Gets or sets whether scrolling with the mousewheel or touch swipe will + /// add a momentum effect to scrolling + /// + public bool UseScrollMomentum + { + get { return this.useScrollMomentum; } + set { this.useScrollMomentum = value; scrollMomentum = Vector2.zero; } + } + + /// + /// Set to TRUE if you want the to scroll when the + /// user presses the arrow keys. + /// + public bool ScrollWithArrowKeys + { + get { return scrollWithArrowKeys; } + set { scrollWithArrowKeys = value; } + } + + /// + /// The Texture Atlas containing the images used by this control + /// + public dfAtlas Atlas + { + get + { + if (this.atlas == null) + { + var view = GetManager(); + if (view != null) + { + return this.atlas = view.DefaultAtlas; + } + } + return this.atlas; + } + set + { + if (!dfAtlas.Equals(value, atlas)) + { + this.atlas = value; + Invalidate(); + } + } + } + + /// + /// The name of the image in the that will be used to + /// render the background of this control + /// + public string BackgroundSprite + { + get { return backgroundSprite; } + set + { + if (value != backgroundSprite) + { + backgroundSprite = value; + Invalidate(); + } + } + } + + /// + /// Gets or set the color that will be applied to the background sprite + /// + public Color32 BackgroundColor + { + get { return backgroundColor; } + set + { + if (!Color32.Equals(value, backgroundColor)) + { + backgroundColor = value; + Invalidate(); + } + } + } + + /// + /// Gets or sets whether the will automatically + /// reset the scrolling region + /// + public bool AutoReset + { + get { return this.autoReset; } + set + { + if (value != this.autoReset) + { + this.autoReset = value; + Reset(); + } + } + } + + /// + /// Gets or sets the amount of padding that will be applied when arranging + /// child controls if the property is set to TRUE + /// + public RectOffset ScrollPadding + { + get + { + if (this.scrollPadding == null) + this.scrollPadding = new RectOffset(); + return this.scrollPadding; + } + set + { + value = value.ConstrainPadding(); + if (!RectOffset.Equals(value, this.scrollPadding)) + { + this.scrollPadding = value; + if (AutoReset || AutoLayout) + Reset(); + } + } + } + + /// + /// Gets or sets whether child controls will be automatically arranged + /// + public bool AutoLayout + { + get { return this.autoLayout; } + set + { + if (value != this.autoLayout) + { + this.autoLayout = value; + if (AutoReset || AutoLayout) + Reset(); + } + } + } + + /// + /// Gets or sets whether controls that lie outside of this container's + /// boundaries will be "wrapped" to the next row or column when using AutoLayout + /// + public bool WrapLayout + { + get { return this.wrapLayout; } + set + { + if (value != this.wrapLayout) + { + this.wrapLayout = value; + Reset(); + } + } + } + + /// + /// Gets or sets the direction in which child controls will be arranged + /// when using AutoLayout + /// + public LayoutDirection FlowDirection + { + get { return this.flowDirection; } + set + { + if (value != this.flowDirection) + { + this.flowDirection = value; + Reset(); + } + } + } + + /// + /// Gets or sets the amount of padding that will be applied to each control + /// when arranging child controls using AutoLayout + /// + public RectOffset FlowPadding + { + get + { + if (this.flowPadding == null) + this.flowPadding = new RectOffset(); + return this.flowPadding; + } + set + { + value = value.ConstrainPadding(); + if (!RectOffset.Equals(value, this.flowPadding)) + { + this.flowPadding = value; + Reset(); + } + } + } + + /// + /// Gets or sets the upper-left position of the viewport relative + /// to the entire scrollable area + /// + public Vector2 ScrollPosition + { + get { return this.scrollPosition; } + set + { + + var viewSize = calculateViewSize(); + var clientSize = new Vector2(this.size.x - this.scrollPadding.horizontal, this.size.y - this.scrollPadding.vertical); + + value = Vector2.Min(viewSize - clientSize, value); + value = Vector2.Max(Vector2.zero, value); + value = value.RoundToInt(); + + if ((value - this.scrollPosition).sqrMagnitude > float.Epsilon) + { + + var delta = value - scrollPosition; + this.scrollPosition = value; + + scrollChildControls(delta); + updateScrollbars(); + + } + + OnScrollPositionChanged(); + + } + } + + /// + /// Gets or sets the distance in pixels that the scroll panel will be scrolled when + /// the user rotates the mouse wheel (this value is overridden by scrollbars, if they + /// are attached) + /// + public int ScrollWheelAmount + { + get { return this.scrollWheelAmount; } + set { this.scrollWheelAmount = value; } + } + + /// + /// Gets or sets a reference the the instance + /// that is used to scroll this container horizontally + /// + public dfScrollbar HorzScrollbar + { + get { return this.horzScroll; } + set + { + horzScroll = value; + updateScrollbars(); + } + } + + /// + /// Gets or sets a reference the the instance + /// that is used to scroll this container vertically + /// + public dfScrollbar VertScrollbar + { + get { return this.vertScroll; } + set + { + vertScroll = value; + updateScrollbars(); + } + } + + /// + /// Indicates the direction to scroll when the user scrolls the mouse wheel + /// + public dfControlOrientation WheelScrollDirection + { + get { return this.wheelDirection; } + set { this.wheelDirection = value; } + } + + /// + /// Gets or sets whether or not the will use a virtual scrolling + /// algorithm for recycling control tiles. + /// + public bool UseVirtualScrolling + { + get { return useVirtualScrolling; } + set + { + useVirtualScrolling = value; + + if (!value) + { + VirtualScrollingTile = null; + } + } + } + + /// + /// Gets or sets whether or not virtualized tiles will be automatically stretched to fit horizontally for vertically + /// scrolling , or vertically for horizontally scrolling controls. + /// + public bool AutoFitVirtualTiles + { + get { return autoFitVirtualTiles; } + set { autoFitVirtualTiles = value; } + } + + /// + /// The to be copied and recycled during virtual scrolling. + /// Attached to this control must be a inherited script that also implements + /// + public dfControl VirtualScrollingTile + { + get { return (useVirtualScrolling) ? virtualScrollingTile : null; } + set { virtualScrollingTile = (useVirtualScrolling) ? value : null; } + } + + #endregion + + #region Overrides + + /// + /// Returns the padding used when clipping is enabled and + /// the renderer is using shader-based clipping + /// + /// + protected internal override RectOffset GetClipPadding() + { + return this.scrollPadding ?? dfRectOffsetExtensions.Empty; + } + + protected internal override Plane[] GetClippingPlanes() + { + + if (!ClipChildren) + return null; + + var corners = GetCorners(); + + var right = transform.TransformDirection(Vector3.right); + var left = transform.TransformDirection(Vector3.left); + var up = transform.TransformDirection(Vector3.up); + var down = transform.TransformDirection(Vector3.down); + + var p2u = PixelsToUnits(); + var padding = ScrollPadding; + corners[0] += right * padding.left * p2u + down * padding.top * p2u; + corners[1] += left * padding.right * p2u + down * padding.top * p2u; + corners[2] += right * padding.left * p2u + up * padding.bottom * p2u; + + return new Plane[] { new Plane( right, corners[ 0 ] ), new Plane( left, corners[ 1 ] ), @@ -484,1330 +486,1372 @@ protected internal override Plane[] GetClippingPlanes() new Plane( down, corners[ 0 ] ) }; - } - - public override bool CanFocus - { - get - { - if( this.IsEnabled && this.IsVisible ) - return true; - return base.CanFocus; - } - } - - public override void OnDestroy() - { - - if( horzScroll != null ) - { - horzScroll.ValueChanged -= horzScroll_ValueChanged; - } - - if( vertScroll != null ) - { - vertScroll.ValueChanged -= vertScroll_ValueChanged; - } - - horzScroll = null; - vertScroll = null; - - } - - public override void Update() - { - - base.Update(); - - if( useScrollMomentum && !isMouseDown && scrollMomentum.magnitude > 0.25f ) - { - ScrollPosition += scrollMomentum; - scrollMomentum *= ( 0.95f - Time.deltaTime ); - } - - if( isControlInvalidated ) - { - - if( autoLayout && IsVisible ) - { - AutoArrange(); - updateScrollbars(); - } - - } - - } - - public override void LateUpdate() - { - - base.LateUpdate(); - - // HACK: Need to perform initialization after all dependant objects - initialize(); + } - if( resetNeeded && IsVisible ) - { + public override bool CanFocus + { + get + { + if (this.IsEnabled && this.IsVisible) + return true; + return base.CanFocus; + } + } - resetNeeded = false; + public override void OnDestroy() + { - if( autoReset || autoLayout ) - { - Reset(); - } + if (horzScroll != null) + { + horzScroll.ValueChanged -= horzScroll_ValueChanged; + } - } + if (vertScroll != null) + { + vertScroll.ValueChanged -= vertScroll_ValueChanged; + } - } + horzScroll = null; + vertScroll = null; - public override void OnEnable() - { + } - base.OnEnable(); + public override void Update() + { - if( this.size == Vector2.zero ) - { - SuspendLayout(); - var camera = GetCamera(); - Size = new Vector3( camera.pixelWidth / 2, camera.pixelHeight / 2 ); - ResumeLayout(); - } + base.Update(); - if( autoLayout ) - { - AutoArrange(); - } + if (useScrollMomentum && !isMouseDown && scrollMomentum.magnitude > 0.25f) + { + ScrollPosition += scrollMomentum; + scrollMomentum *= (0.95f - Time.deltaTime); + } - updateScrollbars(); + if (isControlInvalidated) + { - } + if (autoLayout && IsVisible) + { + AutoArrange(); + updateScrollbars(); + } - protected internal override void OnIsVisibleChanged() - { + } - base.OnIsVisibleChanged(); + } - if( IsVisible && ( autoReset || autoLayout ) ) - { - Reset(); - updateScrollbars(); - } + public override void LateUpdate() + { - } + base.LateUpdate(); - protected internal override void OnSizeChanged() - { + // HACK: Need to perform initialization after all dependant objects + initialize(); - base.OnSizeChanged(); + if (resetNeeded && IsVisible) + { - if( autoReset || autoLayout ) - { - Reset(); - return; - } + resetNeeded = false; - var minScrollPos = calculateMinChildPosition(); - if( minScrollPos.x > scrollPadding.left || minScrollPos.y > scrollPadding.top ) - { + if (autoReset || autoLayout) + { + Reset(); + } - // Adjust minScrollPos to account for padding and clamp it - minScrollPos -= new Vector2( scrollPadding.left, scrollPadding.top ); - minScrollPos = Vector2.Max( minScrollPos, Vector2.zero ); + } - // Scroll child controls to compensate for container resize - scrollChildControls( minScrollPos ); + } - } + public override void OnEnable() + { - updateScrollbars(); + base.OnEnable(); - } + if (this.size == Vector2.zero) + { + SuspendLayout(); + var camera = GetCamera(); + Size = new Vector3(camera.pixelWidth / 2, camera.pixelHeight / 2); + ResumeLayout(); + } - protected internal override void OnResolutionChanged( Vector2 previousResolution, Vector2 currentResolution ) - { - base.OnResolutionChanged( previousResolution, currentResolution ); - resetNeeded = AutoLayout || AutoReset; - } + if (autoLayout) + { + AutoArrange(); + } - protected internal override void OnGotFocus( dfFocusEventArgs args ) - { + updateScrollbars(); - if( args.Source != this ) - { - ScrollIntoView( args.Source ); - } + } - base.OnGotFocus( args ); + protected internal override void OnIsVisibleChanged() + { - } + base.OnIsVisibleChanged(); - protected internal override void OnKeyDown( dfKeyEventArgs args ) - { + if (IsVisible && (autoReset || autoLayout)) + { + Reset(); + updateScrollbars(); + } - if( !scrollWithArrowKeys || args.Used ) - { - base.OnKeyDown( args ); - return; - } + } - var horzAmount = horzScroll != null ? horzScroll.IncrementAmount : 1f; - var vertAmount = vertScroll != null ? vertScroll.IncrementAmount : 1f; + protected internal override void OnSizeChanged() + { - if( args.KeyCode == KeyCode.LeftArrow ) - { - ScrollPosition += new Vector2( -horzAmount, 0 ); - args.Use(); - } - else if( args.KeyCode == KeyCode.RightArrow ) - { - ScrollPosition += new Vector2( horzAmount, 0 ); - args.Use(); - } - else if( args.KeyCode == KeyCode.UpArrow ) - { - ScrollPosition += new Vector2( 0, -vertAmount ); - args.Use(); - } - else if( args.KeyCode == KeyCode.DownArrow ) - { - ScrollPosition += new Vector2( 0, vertAmount ); - args.Use(); - } + base.OnSizeChanged(); - base.OnKeyDown( args ); + if (autoReset || autoLayout) + { + Reset(); + return; + } - } + var minScrollPos = calculateMinChildPosition(); + if (minScrollPos.x > scrollPadding.left || minScrollPos.y > scrollPadding.top) + { - protected internal override void OnMouseEnter( dfMouseEventArgs args ) - { - base.OnMouseEnter( args ); - touchStartPosition = args.Position; - } + // Adjust minScrollPos to account for padding and clamp it + minScrollPos -= new Vector2(scrollPadding.left, scrollPadding.top); + minScrollPos = Vector2.Max(minScrollPos, Vector2.zero); - protected internal override void OnMouseDown( dfMouseEventArgs args ) - { + // Scroll child controls to compensate for container resize + scrollChildControls(minScrollPos); - base.OnMouseDown( args ); + } - scrollMomentum = Vector2.zero; - touchStartPosition = args.Position; - isMouseDown = IsInteractive; + updateScrollbars(); - } + } - internal override void OnDragStart( dfDragEventArgs args ) - { + protected internal override void OnResolutionChanged(Vector2 previousResolution, Vector2 currentResolution) + { + base.OnResolutionChanged(previousResolution, currentResolution); + resetNeeded = AutoLayout || AutoReset; + } - base.OnDragStart( args ); + protected internal override void OnGotFocus(dfFocusEventArgs args) + { - scrollMomentum = Vector2.zero; - if( args.Used ) - { - isMouseDown = false; - } + if (args.Source != this) + { + ScrollIntoView(args.Source); + } - } + base.OnGotFocus(args); - protected internal override void OnMouseUp( dfMouseEventArgs args ) - { - base.OnMouseUp( args ); - isMouseDown = false; - } + } - protected internal override void OnMouseMove( dfMouseEventArgs args ) - { + protected internal override void OnKeyDown(dfKeyEventArgs args) + { - if( args is dfTouchEventArgs || isMouseDown ) - { + if (!scrollWithArrowKeys || args.Used) + { + base.OnKeyDown(args); + return; + } - if( !args.Used && ( args.Position - touchStartPosition ).magnitude > 5 ) - { + var horzAmount = horzScroll != null ? horzScroll.IncrementAmount : 1f; + var vertAmount = vertScroll != null ? vertScroll.IncrementAmount : 1f; - var delta = args.MoveDelta.Scale( -1, 1 ); + if (args.KeyCode == KeyCode.LeftArrow) + { + ScrollPosition += new Vector2(-horzAmount, 0); + args.Use(); + } + else if (args.KeyCode == KeyCode.RightArrow) + { + ScrollPosition += new Vector2(horzAmount, 0); + args.Use(); + } + else if (args.KeyCode == KeyCode.UpArrow) + { + ScrollPosition += new Vector2(0, -vertAmount); + args.Use(); + } + else if (args.KeyCode == KeyCode.DownArrow) + { + ScrollPosition += new Vector2(0, vertAmount); + args.Use(); + } - // Calculate the effective screen size - var manager = GetManager(); - var screenSize = manager.GetScreenSize(); + base.OnKeyDown(args); - // Obtain a reference to the camera used to render this control - var renderCamera = manager.RenderCamera; + } - // Scale the movement amount by the difference between the "virtual" - // screen size and the real screen size - delta.x = screenSize.x * ( delta.x / renderCamera.pixelWidth ); - delta.y = screenSize.y * ( delta.y / renderCamera.pixelHeight ); + protected internal override void OnMouseEnter(dfMouseEventArgs args) + { + base.OnMouseEnter(args); + touchStartPosition = args.Position; + } - // Set the new scroll position and momentum - ScrollPosition += delta; - scrollMomentum = ( scrollMomentum + delta ) * 0.5f; + protected internal override void OnMouseDown(dfMouseEventArgs args) + { - args.Use(); + base.OnMouseDown(args); - } + scrollMomentum = Vector2.zero; + touchStartPosition = args.Position; + isMouseDown = IsInteractive; - } + } - base.OnMouseMove( args ); + internal override void OnDragStart(dfDragEventArgs args) + { - } + base.OnDragStart(args); - protected internal override void OnMouseWheel( dfMouseEventArgs args ) - { + scrollMomentum = Vector2.zero; + if (args.Used) + { + isMouseDown = false; + } - try - { + } - if( args.Used ) - return; - - var amount = wheelDirection == dfControlOrientation.Horizontal - ? horzScroll != null ? horzScroll.IncrementAmount : scrollWheelAmount - : vertScroll != null ? vertScroll.IncrementAmount : scrollWheelAmount; - - if( wheelDirection == dfControlOrientation.Horizontal ) - { - ScrollPosition = new Vector2( scrollPosition.x - amount * args.WheelDelta, scrollPosition.y ); - scrollMomentum = new Vector2( -amount * args.WheelDelta, 0 ); - } - else - { - ScrollPosition = new Vector2( scrollPosition.x, scrollPosition.y - amount * args.WheelDelta ); - scrollMomentum = new Vector2( 0, -amount * args.WheelDelta ); - } - - args.Use(); - Signal( "OnMouseWheel", this, args ); - - } - finally - { - base.OnMouseWheel( args ); - } + protected internal override void OnMouseUp(dfMouseEventArgs args) + { + base.OnMouseUp(args); + isMouseDown = false; + } - } + protected internal override void OnMouseMove(dfMouseEventArgs args) + { - protected internal override void OnControlAdded( dfControl child ) - { + if (args is dfTouchEventArgs || isMouseDown) + { - base.OnControlAdded( child ); + if (!args.Used && (args.Position - touchStartPosition).magnitude > 5) + { - attachEvents( child ); + var delta = args.MoveDelta.Scale(-1, 1); - if( autoLayout ) - { - AutoArrange(); - } + // Calculate the effective screen size + var manager = GetManager(); + var screenSize = manager.GetScreenSize(); - } + // Obtain a reference to the camera used to render this control + var renderCamera = manager.RenderCamera; - protected internal override void OnControlRemoved( dfControl child ) - { + // Scale the movement amount by the difference between the "virtual" + // screen size and the real screen size + delta.x = screenSize.x * (delta.x / renderCamera.pixelWidth); + delta.y = screenSize.y * (delta.y / renderCamera.pixelHeight); - base.OnControlRemoved( child ); + // Set the new scroll position and momentum + ScrollPosition += delta; + scrollMomentum = (scrollMomentum + delta) * 0.5f; - if( child != null ) - { - detachEvents( child ); - } + args.Use(); - if( autoLayout ) - { - AutoArrange(); - } - else - { - updateScrollbars(); - } + } - } + } - protected override void OnRebuildRenderData() - { + base.OnMouseMove(args); - if( Atlas == null || string.IsNullOrEmpty( backgroundSprite ) ) - return; + } - var spriteInfo = Atlas[ backgroundSprite ]; - if( spriteInfo == null ) - { - return; - } + protected internal override void OnMouseWheel(dfMouseEventArgs args) + { - renderData.Material = Atlas.Material; + try + { - var color = ApplyOpacity( this.BackgroundColor ); - var options = new dfSprite.RenderOptions() - { - atlas = atlas, - color = color, - fillAmount = 1, - flip = dfSpriteFlip.None, - offset = pivot.TransformToUpperLeft( Size ), - pixelsToUnits = PixelsToUnits(), - size = Size, - spriteInfo = spriteInfo - }; + if (args.Used) + return; - if( spriteInfo.border.horizontal == 0 && spriteInfo.border.vertical == 0 ) - dfSprite.renderSprite( renderData, options ); - else - dfSlicedSprite.renderSprite( renderData, options ); + var amount = wheelDirection == dfControlOrientation.Horizontal + ? horzScroll != null ? horzScroll.IncrementAmount : scrollWheelAmount + : vertScroll != null ? vertScroll.IncrementAmount : scrollWheelAmount; - } + if (wheelDirection == dfControlOrientation.Horizontal) + { + ScrollPosition = new Vector2(scrollPosition.x - amount * args.WheelDelta, scrollPosition.y); + scrollMomentum = new Vector2(-amount * args.WheelDelta, 0); + } + else + { + ScrollPosition = new Vector2(scrollPosition.x, scrollPosition.y - amount * args.WheelDelta); + scrollMomentum = new Vector2(0, -amount * args.WheelDelta); + } - protected internal void OnScrollPositionChanged() - { + args.Use(); + Signal("OnMouseWheel", this, args); - Invalidate(); + } + finally + { + base.OnMouseWheel(args); + } - SignalHierarchy( "OnScrollPositionChanged", this, this.ScrollPosition ); + } - if( ScrollPositionChanged != null ) - { - ScrollPositionChanged( this, this.ScrollPosition ); - } + protected internal override void OnControlAdded(dfControl child) + { - } + base.OnControlAdded(child); - #endregion + attachEvents(child); - #region Public methods + if (autoLayout) + { + AutoArrange(); + } - /// - /// Resizes the panel to ensure that it encompasses all child controls - /// - public void FitToContents() - { + } - if( controls.Count == 0 ) - return; + protected internal override void OnControlRemoved(dfControl child) + { - var max = Vector2.zero; - for( int i = 0; i < controls.Count; i++ ) - { + base.OnControlRemoved(child); - var child = controls[ i ]; - var childMax = (Vector2)child.RelativePosition + child.Size; + if (child != null) + { + detachEvents(child); + } - max = Vector2.Max( max, childMax ); + if (autoLayout) + { + AutoArrange(); + } + else + { + updateScrollbars(); + } - } + } - this.Size = max + new Vector2( scrollPadding.right, scrollPadding.bottom ); + protected override void OnRebuildRenderData() + { - } + if (Atlas == null || string.IsNullOrEmpty(backgroundSprite)) + return; - /// - /// Centers all child controls within the bounds of the panel - /// - public void CenterChildControls() - { + var spriteInfo = Atlas[backgroundSprite]; + if (spriteInfo == null) + { + return; + } - if( controls.Count == 0 ) - return; + renderData.Material = Atlas.Material; - var min = Vector2.one * float.MaxValue; - var max = Vector2.one * float.MinValue; + var color = ApplyOpacity(this.BackgroundColor); + var options = new dfSprite.RenderOptions() + { + atlas = atlas, + color = color, + fillAmount = 1, + flip = dfSpriteFlip.None, + offset = pivot.TransformToUpperLeft(Size), + pixelsToUnits = PixelsToUnits(), + size = Size, + spriteInfo = spriteInfo + }; - for( int i = 0; i < controls.Count; i++ ) - { + if (spriteInfo.border.horizontal == 0 && spriteInfo.border.vertical == 0) + dfSprite.renderSprite(renderData, options); + else + dfSlicedSprite.renderSprite(renderData, options); - var child = controls[ i ]; - var childMin = (Vector2)child.RelativePosition; - var childMax = childMin + child.Size; + } - min = Vector2.Min( min, childMin ); - max = Vector2.Max( max, childMax ); + protected internal void OnScrollPositionChanged() + { - } + Invalidate(); - var contentSize = max - min; - var contentOffset = ( this.Size - contentSize ) * 0.5f; + SignalHierarchy("OnScrollPositionChanged", this, this.ScrollPosition); - for( int i = 0; i < controls.Count; i++ ) - { - var child = controls[ i ]; - child.RelativePosition = (Vector2)child.RelativePosition - min + contentOffset; - } - - } - - /// - /// Sets the scrollposition to the top - /// - public void ScrollToTop() - { - scrollMomentum = Vector2.zero; - this.ScrollPosition = new Vector2( this.scrollPosition.x, 0 ); - } - - /// - /// Sets the scrollposition to the top - /// - public void ScrollToBottom() - { - scrollMomentum = Vector2.zero; - this.ScrollPosition = new Vector2( this.scrollPosition.x, int.MaxValue ); - } - - /// - /// Sets the scrollposition to the top - /// - public void ScrollToLeft() - { - scrollMomentum = Vector2.zero; - this.ScrollPosition = new Vector2( 0, this.scrollPosition.y ); - } - - /// - /// Sets the scrollposition to the top - /// - public void ScrollToRight() - { - scrollMomentum = Vector2.zero; - this.ScrollPosition = new Vector2( int.MaxValue, this.scrollPosition.y ); - } - - /// - /// Scrolls the specified child control into view - /// - /// The child control to scroll into view - public void ScrollIntoView( dfControl control ) - { - - scrollMomentum = Vector2.zero; - - var viewRect = new Rect( - scrollPosition.x + scrollPadding.left, - scrollPosition.y + scrollPadding.top, - size.x - scrollPadding.horizontal, - size.y - scrollPadding.vertical - ).RoundToInt(); - - var controlPosition = control.RelativePosition; - var controlSize = control.Size; - - while( !controls.Contains( control ) ) - { - control = control.Parent; - controlPosition += control.RelativePosition; - } - - var controlRect = new Rect( - scrollPosition.x + controlPosition.x, - scrollPosition.y + controlPosition.y, - controlSize.x, - controlSize.y - ).RoundToInt(); - - if( viewRect.Contains( controlRect ) ) - { - return; - } + if (ScrollPositionChanged != null) + { + ScrollPositionChanged(this, this.ScrollPosition); + } + + } - var newScrollPos = scrollPosition; + #endregion - if( controlRect.xMin < viewRect.xMin ) - { - newScrollPos.x = controlRect.xMin - scrollPadding.left; - } - else if( controlRect.xMax > viewRect.xMax ) - { - newScrollPos.x = controlRect.xMax - Mathf.Max( size.x, controlSize.x ) + scrollPadding.horizontal; - } + #region Public methods - if( controlRect.y < viewRect.y ) - { - newScrollPos.y = controlRect.yMin - scrollPadding.top; - } - else if( controlRect.yMax > viewRect.yMax ) - { - newScrollPos.y = controlRect.yMax - Mathf.Max( size.y, controlSize.y ) + scrollPadding.vertical; - } + /// + /// Resizes the panel to ensure that it encompasses all child controls + /// + public void FitToContents() + { - ScrollPosition = newScrollPos; - scrollMomentum = Vector2.zero; + if (controls.Count == 0) + return; - } + var max = Vector2.zero; + for (int i = 0; i < controls.Count; i++) + { - /// - /// Reset the viewport back to the upper left origin of the scrollable area - /// - public void Reset() - { + var child = controls[i]; + var childMax = (Vector2)child.RelativePosition + child.Size; - try - { + max = Vector2.Max(max, childMax); + + } + + this.Size = max + new Vector2(scrollPadding.right, scrollPadding.bottom); + + } + + /// + /// Centers all child controls within the bounds of the panel + /// + public void CenterChildControls() + { + + if (controls.Count == 0) + return; + + var min = Vector2.one * float.MaxValue; + var max = Vector2.one * float.MinValue; + + for (int i = 0; i < controls.Count; i++) + { + + var child = controls[i]; + var childMin = (Vector2)child.RelativePosition; + var childMax = childMin + child.Size; + + min = Vector2.Min(min, childMin); + max = Vector2.Max(max, childMax); + + } + + var contentSize = max - min; + var contentOffset = (this.Size - contentSize) * 0.5f; + + for (int i = 0; i < controls.Count; i++) + { + var child = controls[i]; + child.RelativePosition = (Vector2)child.RelativePosition - min + contentOffset; + } + + } + + /// + /// Sets the scrollposition to the top + /// + public void ScrollToTop() + { + scrollMomentum = Vector2.zero; + this.ScrollPosition = new Vector2(this.scrollPosition.x, 0); + } + + /// + /// Sets the scrollposition to the top + /// + public void ScrollToBottom() + { + scrollMomentum = Vector2.zero; + this.ScrollPosition = new Vector2(this.scrollPosition.x, int.MaxValue); + } + + /// + /// Sets the scrollposition to the top + /// + public void ScrollToLeft() + { + scrollMomentum = Vector2.zero; + this.ScrollPosition = new Vector2(0, this.scrollPosition.y); + } + + /// + /// Sets the scrollposition to the top + /// + public void ScrollToRight() + { + scrollMomentum = Vector2.zero; + this.ScrollPosition = new Vector2(int.MaxValue, this.scrollPosition.y); + } + + /// + /// Scrolls the specified child control into view + /// + /// The child control to scroll into view + public void ScrollIntoView(dfControl control) + { + + scrollMomentum = Vector2.zero; + + var viewRect = new Rect( + scrollPosition.x + scrollPadding.left, + scrollPosition.y + scrollPadding.top, + size.x - scrollPadding.horizontal, + size.y - scrollPadding.vertical + ).RoundToInt(); + + var controlPosition = control.RelativePosition; + var controlSize = control.Size; + + while (!controls.Contains(control)) + { + control = control.Parent; + controlPosition += control.RelativePosition; + } + + var controlRect = new Rect( + scrollPosition.x + controlPosition.x, + scrollPosition.y + controlPosition.y, + controlSize.x, + controlSize.y + ).RoundToInt(); + + if (viewRect.Contains(controlRect)) + { + return; + } + + var newScrollPos = scrollPosition; + + if (controlRect.xMin < viewRect.xMin) + { + newScrollPos.x = controlRect.xMin - scrollPadding.left; + } + else if (controlRect.xMax > viewRect.xMax) + { + newScrollPos.x = controlRect.xMax - Mathf.Max(size.x, controlSize.x) + scrollPadding.horizontal; + } + + if (controlRect.y < viewRect.y) + { + newScrollPos.y = controlRect.yMin - scrollPadding.top; + } + else if (controlRect.yMax > viewRect.yMax) + { + newScrollPos.y = controlRect.yMax - Mathf.Max(size.y, controlSize.y) + scrollPadding.vertical; + } + + ScrollPosition = newScrollPos; + scrollMomentum = Vector2.zero; + + } + + /// + /// Reset the viewport back to the upper left origin of the scrollable area + /// + public void Reset() + { + + try + { + + SuspendLayout(); + + if (autoLayout) + { + var savedScrollPosition = this.ScrollPosition; + this.ScrollPosition = Vector2.zero; + AutoArrange(); + ScrollPosition = savedScrollPosition; + } + else + { + + scrollPadding = ScrollPadding.ConstrainPadding(); + + var offset = (Vector3)calculateMinChildPosition(); + offset -= new Vector3(scrollPadding.left, scrollPadding.top); + + for (int i = 0; i < controls.Count; i++) + { + controls[i].RelativePosition -= offset; + } + + scrollPosition = Vector2.zero; + + } + + Invalidate(); + + updateScrollbars(); + + } + finally + { + ResumeLayout(); + } + + } + + /// + /// Instruct the dfScrollPanel to only create as many tiles as needed to fill the dfScrollPanel, + /// and recycle the information presented on the tile based on the backing list. + /// + /// + /// An arbitrary list of objects that will seed the tiles as they are recycled / initialized. + /// + /// Used internally; passed from when ScrollBar paging begins. + /// Once I can successfully get it working for initially starting the virtualization at this point + /// I will publicly expose this method. + /// + private void Virtualize(IList backingList, int startIndex) + { + + if (!useVirtualScrolling) + { + Debug.LogError("Virtual scrolling not enabled for this dfScrollPanel: " + name); + return; + } + + if (virtualScrollingTile == null) + { + Debug.LogError("Virtual scrolling cannot be started without assigning VirtualScrollingTile: " + name); + return; + } + + if (backingList.Count == 0) + { + //# What do you think we should we do here, if anything? + } + + var data = GetVirtualScrollData() ?? initVirtualScrollData(backingList); + var isVerticalFlow = this.isVerticalFlow(); + + //# Used to save the flow padding if they had auto-layout on in the editor. + var padding = data.ItemPadding = new RectOffset( + FlowPadding.left, + FlowPadding.right, + FlowPadding.top, + FlowPadding.bottom); + + //# Directional padding. + var dPadding = (isVerticalFlow) ? padding.vertical : padding.horizontal; + //# 'Zero' padding. Couldn't think of a more succinct variable name ;) + var zPadding = (isVerticalFlow) ? padding.top : padding.left; + //# ScrollPanel measurement for direction. + var spLength = (isVerticalFlow) ? Height : Width; + + //# We no longer need these as we handle layouting, and resetting manually. + AutoLayout = false; + AutoReset = false; + + //# Dummy tile to get measurements. Skip initializing one if we are paging (already have one). + var dummy = data.DummyTop ?? (data.DummyTop = initTile(padding)); + var dp = dummy.GetDfPanel(); + var tileLength = (isVerticalFlow) + ? dummy.GetDfPanel().Height + : dummy.GetDfPanel().Width; + + //# Cleanup for the top dummy. + dp.IsEnabled = false; + dp.Opacity = 0; + dp.gameObject.hideFlags = HideFlags.HideInHierarchy; + lastWidth = tileLength + dPadding; + //# We need to do more "dummy" hacking if user is using scrollbars so the scrollbar sees a "max scroll position". + //# We can skip this whole block if we are paging (already know we have scrollbars). + dfScrollbar sb; + + if ((sb = VertScrollbar) || (sb = HorzScrollbar)) + { + + var bottomDummy = data.DummyBottom ?? (data.DummyBottom = initTile(padding)); + var bdp = bottomDummy.GetDfPanel(); + + //# Shoot to bottom so scrollbar sees a maxvalue + var dpStart = (isVerticalFlow) ? dp.RelativePosition.y : dp.RelativePosition.x; + var bdpPos = dpStart + (((backingList.Count - 1) * (tileLength + dPadding)) + zPadding); + + //# Send all the way to the very bottom where it should meet up with the last reycled tile in our list. + bdp.RelativePosition = (isVerticalFlow) + ? new Vector3(dp.RelativePosition.x, bdpPos) + : new Vector3(bdpPos, dp.RelativePosition.y); + + //# Mirroring other dummy panel so I can quickly debug. + bdp.IsEnabled = dp.IsEnabled; + bdp.gameObject.hideFlags = dp.hideFlags; + bdp.Opacity = dp.Opacity; + + //# This block ensures the virtual scrol position is saved when new items are added / removed from the backing list. + if (startIndex == 0 && sb.MaxValue != 0) + { + var pct = sb.Value / sb.MaxValue; + //# I've tried ceil and floor, but the results are kinda unpredictable sometimes. Rounding seems to be the best so far + //# without messing about with float tolerance. Mostly annoying after paging, and them touch scrolling back to 0; + //# sometimes the 0th tile is a tad off-y/x + startIndex = Mathf.RoundToInt(pct * (backingList.Count - 1)); + } + + //# Give the scrollbar a perfect value so that non-paged scrolling doesn't mess up the 0th tile. + sb.Value = startIndex * (tileLength + dPadding); + } + + //# Some checks here to determine the optimal number of tiles to generate in order to make virtualization possible. + var maxTilesRaw = spLength / (tileLength + dPadding); + var maxTilesRounded = Mathf.RoundToInt(maxTilesRaw); + var maxTiles = (maxTilesRounded > maxTilesRaw) + ? maxTilesRounded + 1 + : maxTilesRounded + 2; + + //# Initially I was going to use this for paging issues, but no longer neccessary. + //# Could use if for "flickering" caused by recycling if needed, but we'll have to see where best to + //# make this publicly available. + //maxTiles += data.MaxExtraOffscreenTiles; + + //# Begin magic + float nextZero = zPadding; + //# I'm going to work on getting the ScrollBar and the ScrollPanel to reflect these changes. So far, no luck. + float startScrollAtIndex = startIndex; + + //# Loop throug all of our tiles (or potential tiles) and place them evenly on the stage. + for (var i = 0; i < maxTiles && i < backingList.Count && startIndex <= backingList.Count; i++) + { + //# We use a try/catch since we need to be notified, and correct index errors. + try + { + var tile = (data.IsInitialized && data.Tiles.Count >= (i + 1)) + ? data.Tiles[i] + : initTile(padding); + var panel = tile.GetDfPanel(); + var zero = nextZero; + + panel.RelativePosition = (isVerticalFlow) + ? new Vector2(padding.left, zero) + : new Vector2(zero, padding.top); + nextZero = zero + tileLength + dPadding; + + if (!data.Tiles.Contains(tile)) + { + data.Tiles.Add(tile); + } + + tile.VirtualScrollItemIndex = startIndex; + tile.OnScrollPanelItemVirtualize(backingList[startIndex]); + + startIndex++; + } + catch + { + foreach (var tile in data.Tiles) + { + var index = tile.VirtualScrollItemIndex - 1; + + tile.VirtualScrollItemIndex = index; + tile.OnScrollPanelItemVirtualize(backingList[index]); + } + } + } + + //# Remove the old listener so we aren't doubling up... + //# Eventually I will also use this block to set the scrollbar position for users who want to start + //# at a certian index in their backing list. + if (startScrollAtIndex != 0) + { + if (ScrollPositionChanged != null) + { + ScrollPositionChanged -= virtualScrollPositionChanged; + } + } + + data.IsInitialized = true; + ScrollPositionChanged += virtualScrollPositionChanged; + + } + + /// + /// Instruct the dfScrollPanel to only create as many tiles as needed to fill the dfScrollPanel, + /// and recycle the information presented on the tile based on the backing list. + /// + /// + /// An arbitrary list of objects that will seed the tiles as they are recycled / initialized. + /// A dfPanel tile. + public void Virtualize(IList backingList, dfPanel tile) + { + + var inter = tile + .GetComponents() + .FirstOrDefault(t => t is IDFVirtualScrollingTile); + + if (!inter) + { + Debug.LogError("The tile you've chosen does not implement IDFVirtualScrollingTile!"); + return; + } + + UseVirtualScrolling = true; + VirtualScrollingTile = tile; + + Virtualize(backingList, 0); + + } + + /// + /// Instruct the dfScrollPanel to only create as many tiles as needed to fill the dfScrollPanel, + /// and recycle the information presented on the tile based on the backing list. + /// + /// + /// An arbitrary list of objects that will seed the tiles as they are recycled / initialized. + public void Virtualize(IList backingList) + { + Virtualize(backingList, 0); + } + + public void ResetVirtualScrollingData() + { + + virtualScrollData = null; + + var temp = controls.ToArray(); + + for (int i = 0; i < temp.Length; i++) + { + var c = temp[i]; + RemoveControl(c); + c.transform.parent = null; + Destroy(c.gameObject, 0.2f * i); + + } + + ScrollPosition = Vector2.zero; + + } + + /// + /// Get a reference to the store virtual scrolling information. + /// + /// Where T is the Type parameter in the backing list seeding the tiles. + /// + public dfVirtualScrollData GetVirtualScrollData() + { + return (dfVirtualScrollData)virtualScrollData; + } + + #endregion + + #region Private utility methods + + [HideInInspector] + private void AutoArrange() + { + + SuspendLayout(); + try + { + + scrollPadding = ScrollPadding.ConstrainPadding(); + flowPadding = FlowPadding.ConstrainPadding(); + + var x = (float)scrollPadding.left + (float)flowPadding.left - scrollPosition.x; + var y = (float)scrollPadding.top + (float)flowPadding.top - scrollPosition.y; - SuspendLayout(); + var maxWidth = 0f; + var maxHeight = 0f; - if( autoLayout ) - { - var savedScrollPosition = this.ScrollPosition; - this.ScrollPosition = Vector2.zero; - AutoArrange(); - ScrollPosition = savedScrollPosition; - } - else - { + for (int i = 0; i < controls.Count; i++) + { - scrollPadding = ScrollPadding.ConstrainPadding(); + var child = controls[i]; - var offset = (Vector3)calculateMinChildPosition(); - offset -= new Vector3( scrollPadding.left, scrollPadding.top ); + if (!child.GetIsVisibleRaw() || !child.enabled || !child.gameObject.activeSelf) + continue; - for( int i = 0; i < controls.Count; i++ ) - { - controls[ i ].RelativePosition -= offset; - } + if (child == this.horzScroll || child == this.vertScroll) + continue; - scrollPosition = Vector2.zero; + if (this.wrapLayout) + { - } + if (flowDirection == LayoutDirection.Horizontal) + { + if (x + child.Width >= size.x - scrollPadding.right) + { + x = (float)scrollPadding.left + (float)flowPadding.left; + y += maxHeight; + maxHeight = 0f; + } + } + else + { + if (y + child.Height + flowPadding.vertical >= size.y - scrollPadding.bottom) + { + y = (float)scrollPadding.top + (float)flowPadding.top; + x += maxWidth; + maxWidth = 0f; + } + } - Invalidate(); + } - updateScrollbars(); + var childPosition = new Vector2(x, y); + child.RelativePosition = childPosition; - } - finally - { - ResumeLayout(); - } - - } - - /// - /// Instruct the dfScrollPanel to only create as many tiles as needed to fill the dfScrollPanel, - /// and recycle the information presented on the tile based on the backing list. - /// - /// - /// An arbitrary list of objects that will seed the tiles as they are recycled / initialized. - /// - /// Used internally; passed from when ScrollBar paging begins. - /// Once I can successfully get it working for initially starting the virtualization at this point - /// I will publicly expose this method. - /// - private void Virtualize( IList backingList, int startIndex ) - { - - if( !useVirtualScrolling ) - { - Debug.LogError( "Virtual scrolling not enabled for this dfScrollPanel: " + name ); - return; - } - - if( virtualScrollingTile == null ) - { - Debug.LogError( "Virtual scrolling cannot be started without assigning VirtualScrollingTile: " + name ); - return; - } - - if( backingList.Count == 0 ) - { - //# What do you think we should we do here, if anything? - } - - var data = GetVirtualScrollData() ?? initVirtualScrollData( backingList ); - var isVerticalFlow = this.isVerticalFlow(); - - //# Used to save the flow padding if they had auto-layout on in the editor. - var padding = data.ItemPadding = new RectOffset( - FlowPadding.left, - FlowPadding.right, - FlowPadding.top, - FlowPadding.bottom ); - - //# Directional padding. - var dPadding = ( isVerticalFlow ) ? padding.vertical : padding.horizontal; - //# 'Zero' padding. Couldn't think of a more succinct variable name ;) - var zPadding = ( isVerticalFlow ) ? padding.top : padding.left; - //# ScrollPanel measurement for direction. - var spLength = ( isVerticalFlow ) ? Height : Width; - - //# We no longer need these as we handle layouting, and resetting manually. - AutoLayout = false; - AutoReset = false; - - //# Dummy tile to get measurements. Skip initializing one if we are paging (already have one). - var dummy = data.DummyTop ?? ( data.DummyTop = initTile( padding ) ); - var dp = dummy.GetDfPanel(); - var tileLength = ( isVerticalFlow ) - ? dummy.GetDfPanel().Height - : dummy.GetDfPanel().Width; - - //# Cleanup for the top dummy. - dp.IsEnabled = false; - dp.Opacity = 0; - dp.gameObject.hideFlags = HideFlags.HideInHierarchy; - - //# We need to do more "dummy" hacking if user is using scrollbars so the scrollbar sees a "max scroll position". - //# We can skip this whole block if we are paging (already know we have scrollbars). - dfScrollbar sb; - - if( ( sb = VertScrollbar ) || ( sb = HorzScrollbar ) ) - { - - var bottomDummy = data.DummyBottom ?? ( data.DummyBottom = initTile( padding ) ); - var bdp = bottomDummy.GetDfPanel(); - - //# Shoot to bottom so scrollbar sees a maxvalue - var dpStart = ( isVerticalFlow ) ? dp.RelativePosition.y : dp.RelativePosition.x; - var bdpPos = dpStart + ( ( ( backingList.Count - 1 ) * ( tileLength + dPadding ) ) + zPadding ); - - //# Send all the way to the very bottom where it should meet up with the last reycled tile in our list. - bdp.RelativePosition = ( isVerticalFlow ) - ? new Vector3( dp.RelativePosition.x, bdpPos ) - : new Vector3( bdpPos, dp.RelativePosition.y ); - - //# Mirroring other dummy panel so I can quickly debug. - bdp.IsEnabled = dp.IsEnabled; - bdp.gameObject.hideFlags = dp.hideFlags; - bdp.Opacity = dp.Opacity; - - //# This block ensures the virtual scrol position is saved when new items are added / removed from the backing list. - if( startIndex == 0 && sb.MaxValue != 0 ) - { - var pct = sb.Value / sb.MaxValue; - //# I've tried ceil and floor, but the results are kinda unpredictable sometimes. Rounding seems to be the best so far - //# without messing about with float tolerance. Mostly annoying after paging, and them touch scrolling back to 0; - //# sometimes the 0th tile is a tad off-y/x - startIndex = Mathf.RoundToInt( pct * ( backingList.Count - 1 ) ); - } - - //# Give the scrollbar a perfect value so that non-paged scrolling doesn't mess up the 0th tile. - sb.Value = startIndex * ( tileLength + dPadding ); - } - - //# Some checks here to determine the optimal number of tiles to generate in order to make virtualization possible. - var maxTilesRaw = spLength / ( tileLength + dPadding ); - var maxTilesRounded = Mathf.RoundToInt( maxTilesRaw ); - var maxTiles = ( maxTilesRounded > maxTilesRaw ) - ? maxTilesRounded + 1 - : maxTilesRounded + 2; - - //# Initially I was going to use this for paging issues, but no longer neccessary. - //# Could use if for "flickering" caused by recycling if needed, but we'll have to see where best to - //# make this publicly available. - //maxTiles += data.MaxExtraOffscreenTiles; - - //# Begin magic - float nextZero = zPadding; - //# I'm going to work on getting the ScrollBar and the ScrollPanel to reflect these changes. So far, no luck. - float startScrollAtIndex = startIndex; - - //# Loop throug all of our tiles (or potential tiles) and place them evenly on the stage. - for( var i = 0; i < maxTiles && i < backingList.Count && startIndex <= backingList.Count; i++ ) - { - //# We use a try/catch since we need to be notified, and correct index errors. - try - { - var tile = ( data.IsInitialized && data.Tiles.Count >= ( i + 1 ) ) - ? data.Tiles[ i ] - : initTile( padding ); - var panel = tile.GetDfPanel(); - var zero = nextZero; - - panel.RelativePosition = ( isVerticalFlow ) - ? new Vector2( padding.left, zero ) - : new Vector2( zero, padding.top ); - nextZero = zero + tileLength + dPadding; - - if( !data.Tiles.Contains( tile ) ) - { - data.Tiles.Add( tile ); - } - - tile.VirtualScrollItemIndex = startIndex; - tile.OnScrollPanelItemVirtualize( backingList[ startIndex ] ); - - startIndex++; - } - catch - { - foreach( var tile in data.Tiles ) - { - var index = tile.VirtualScrollItemIndex - 1; - - tile.VirtualScrollItemIndex = index; - tile.OnScrollPanelItemVirtualize( backingList[ index ] ); - } - } - } - - //# Remove the old listener so we aren't doubling up... - //# Eventually I will also use this block to set the scrollbar position for users who want to start - //# at a certian index in their backing list. - if( startScrollAtIndex != 0 ) - { - if( ScrollPositionChanged != null ) - { - ScrollPositionChanged -= virtualScrollPositionChanged; - } - } - - data.IsInitialized = true; - ScrollPositionChanged += virtualScrollPositionChanged; - - } - - /// - /// Instruct the dfScrollPanel to only create as many tiles as needed to fill the dfScrollPanel, - /// and recycle the information presented on the tile based on the backing list. - /// - /// - /// An arbitrary list of objects that will seed the tiles as they are recycled / initialized. - /// A dfPanel tile. - public void Virtualize( IList backingList, dfPanel tile ) - { - - var inter = tile - .GetComponents() - .FirstOrDefault( t => t is IDFVirtualScrollingTile ); - - if( !inter ) - { - Debug.LogError( "The tile you've chosen does not implement IDFVirtualScrollingTile!" ); - return; - } - - UseVirtualScrolling = true; - VirtualScrollingTile = tile; - - Virtualize( backingList, 0 ); - - } - - /// - /// Instruct the dfScrollPanel to only create as many tiles as needed to fill the dfScrollPanel, - /// and recycle the information presented on the tile based on the backing list. - /// - /// - /// An arbitrary list of objects that will seed the tiles as they are recycled / initialized. - public void Virtualize( IList backingList ) - { - Virtualize( backingList, 0 ); - } - - public void ResetVirtualScrollingData() - { - - virtualScrollData = null; - - var temp = controls.ToArray(); - - for( int i = 0; i < temp.Length; i++ ) - { - var c = temp[ i ]; - RemoveControl( c ); - Destroy( c.gameObject ); - } + var xofs = child.Width + flowPadding.horizontal; + var yofs = child.Height + flowPadding.vertical; - ScrollPosition = Vector2.zero; + maxWidth = Mathf.Max(xofs, maxWidth); + maxHeight = Mathf.Max(yofs, maxHeight); - } + if (flowDirection == LayoutDirection.Horizontal) + x += xofs; + else + y += yofs; - /// - /// Get a reference to the store virtual scrolling information. - /// - /// Where T is the Type parameter in the backing list seeding the tiles. - /// - public dfVirtualScrollData GetVirtualScrollData() - { - return (dfVirtualScrollData) virtualScrollData; - } + } - #endregion + updateScrollbars(); - #region Private utility methods + } + finally + { + ResumeLayout(); + } - [HideInInspector] - private void AutoArrange() - { + } - SuspendLayout(); - try - { + [HideInInspector] + private void initialize() + { - scrollPadding = ScrollPadding.ConstrainPadding(); - flowPadding = FlowPadding.ConstrainPadding(); + if (initialized) + return; - var x = (float)scrollPadding.left + (float)flowPadding.left - scrollPosition.x; - var y = (float)scrollPadding.top + (float)flowPadding.top - scrollPosition.y; + initialized = true; - var maxWidth = 0f; - var maxHeight = 0f; + if (Application.isPlaying) + { - for( int i = 0; i < controls.Count; i++ ) - { + if (horzScroll != null) + { + horzScroll.ValueChanged += horzScroll_ValueChanged; + } - var child = controls[ i ]; + if (vertScroll != null) + { + vertScroll.ValueChanged += vertScroll_ValueChanged; + } - if( !child.GetIsVisibleRaw() || !child.enabled || !child.gameObject.activeSelf ) - continue; + } - if( child == this.horzScroll || child == this.vertScroll ) - continue; + if (resetNeeded || autoLayout || autoReset) + { + Reset(); + } - if( this.wrapLayout ) - { + Invalidate(); + ScrollPosition = Vector2.zero; - if( flowDirection == LayoutDirection.Horizontal ) - { - if( x + child.Width >= size.x - scrollPadding.right ) - { - x = (float)scrollPadding.left + (float)flowPadding.left; - y += maxHeight; - maxHeight = 0f; - } - } - else - { - if( y + child.Height + flowPadding.vertical >= size.y - scrollPadding.bottom ) - { - y = (float)scrollPadding.top + (float)flowPadding.top; - x += maxWidth; - maxWidth = 0f; - } - } + updateScrollbars(); - } + } - var childPosition = new Vector2( x, y ); - child.RelativePosition = childPosition; + private void vertScroll_ValueChanged(dfControl control, float value) + { + ScrollPosition = new Vector2(scrollPosition.x, value); + } - var xofs = child.Width + flowPadding.horizontal; - var yofs = child.Height + flowPadding.vertical; + private void horzScroll_ValueChanged(dfControl control, float value) + { + ScrollPosition = new Vector2(value, ScrollPosition.y); + } - maxWidth = Mathf.Max( xofs, maxWidth ); - maxHeight = Mathf.Max( yofs, maxHeight ); + private void scrollChildControls(Vector3 delta) + { - if( flowDirection == LayoutDirection.Horizontal ) - x += xofs; - else - y += yofs; + try + { - } + scrolling = true; - updateScrollbars(); + delta = delta.Scale(1, -1, 1); - } - finally - { - ResumeLayout(); - } + for (int i = 0; i < controls.Count; i++) + { + var child = controls[i]; + child.Position = (child.Position - delta).RoundToInt(); + } - } + } + finally + { + scrolling = false; + } - [HideInInspector] - private void initialize() - { + } - if( initialized ) - return; + private Vector2 calculateMinChildPosition() + { - initialized = true; + var minX = float.MaxValue; + var minY = float.MaxValue; - if( Application.isPlaying ) - { + for (int i = 0; i < controls.Count; i++) + { - if( horzScroll != null ) - { - horzScroll.ValueChanged += horzScroll_ValueChanged; - } + var child = controls[i]; + if (!child.enabled || !child.gameObject.activeSelf) + continue; - if( vertScroll != null ) - { - vertScroll.ValueChanged += vertScroll_ValueChanged; - } + var childPos = child.RelativePosition.FloorToInt(); + minX = Mathf.Min(minX, childPos.x); + minY = Mathf.Min(minY, childPos.y); - } + } - if( resetNeeded || autoLayout || autoReset ) - { - Reset(); - } + return new Vector2(minX, minY); - Invalidate(); - ScrollPosition = Vector2.zero; + } - updateScrollbars(); + private Vector2 calculateViewSize() + { - } + // Calculate size of client rect + var padding = new Vector2(scrollPadding.horizontal, scrollPadding.vertical).RoundToInt(); + var clientSize = this.Size.RoundToInt() - padding; - private void vertScroll_ValueChanged( dfControl control, float value ) - { - ScrollPosition = new Vector2( scrollPosition.x, value ); - } + // If not visible or no controls, viewsize is same as client rect + if (!this.IsVisible || this.controls.Count == 0) + { + return clientSize; + } - private void horzScroll_ValueChanged( dfControl control, float value ) - { - ScrollPosition = new Vector2( value, ScrollPosition.y ); - } + var min = Vector2.one * float.MaxValue; + var max = Vector2.one * -float.MaxValue; - private void scrollChildControls( Vector3 delta ) - { + for (int i = 0; i < controls.Count; i++) + { - try - { + var child = controls[i]; - scrolling = true; + // Skip calculation of child controls that are not visible. + // NOTE: Only done during runtime, as this control is "live" + // in the editor and we don't want to change the layout + // during design time. Everything will be correct when running. + if (Application.isPlaying && !child.IsVisible) + continue; - delta = delta.Scale( 1, -1, 1 ); + var controlMin = (Vector2)child.RelativePosition.CeilToInt(); + var controlMax = controlMin + child.Size.CeilToInt(); - for( int i = 0; i < controls.Count; i++ ) - { - var child = controls[ i ]; - child.Position = ( child.Position - delta ).RoundToInt(); - } + min = Vector2.Min(controlMin, min); + max = Vector2.Max(controlMax, max); - } - finally - { - scrolling = false; - } + } - } + // If the minimum control position is greater than the origin, we'll need + // to compensate for that so that controls don't get moved to the origin + // and to allow for scrolling all the way to the far extents + var minOffset = Vector2.Max(Vector2.zero, min - new Vector2(scrollPadding.left, scrollPadding.top)); - private Vector2 calculateMinChildPosition() - { + // Regardless of where the current scroll position is, the + // max view extent is always the lower-right corner of the + // client rect. + max = Vector2.Max(max + minOffset, clientSize); - var minX = float.MaxValue; - var minY = float.MaxValue; + return (max - min) + minOffset; - for( int i = 0; i < controls.Count; i++ ) - { + } - var child = controls[ i ]; - if( !child.enabled || !child.gameObject.activeSelf ) - continue; + [HideInInspector] + private void updateScrollbars() + { - var childPos = child.RelativePosition.FloorToInt(); - minX = Mathf.Min( minX, childPos.x ); - minY = Mathf.Min( minY, childPos.y ); + var viewSize = calculateViewSize(); - } + var clientSize = this.Size - new Vector2(scrollPadding.horizontal, scrollPadding.vertical); - return new Vector2( minX, minY ); + if (horzScroll != null) + { + horzScroll.MinValue = 0; + horzScroll.MaxValue = viewSize.x; + horzScroll.ScrollSize = clientSize.x; + horzScroll.Value = Mathf.Max(0, scrollPosition.x); + } - } + if (vertScroll != null) + { + vertScroll.MinValue = 0; + vertScroll.MaxValue = viewSize.y; + vertScroll.ScrollSize = clientSize.y; + vertScroll.Value = Mathf.Max(0, scrollPosition.y); + } - private Vector2 calculateViewSize() - { + } - // Calculate size of client rect - var padding = new Vector2( scrollPadding.horizontal, scrollPadding.vertical ).RoundToInt(); - var clientSize = this.Size.RoundToInt() - padding; + /// + /// This method triggers when the scroll position changes. It checks all of the tiles, and + /// performs recycling on them if they are outside the directional bounds. + /// + /// + /// + /// + private void virtualScrollPositionChanged(dfControl control, Vector2 value) + { - // If not visible or no controls, viewsize is same as client rect - if( !this.IsVisible || this.controls.Count == 0 ) - { - return clientSize; - } + var data = GetVirtualScrollData(); + + if (data == null) + { + return; + } + + var list = data.BackingList; + var padding = data.ItemPadding; + var tiles = data.Tiles; + var isVerticalFlow = this.isVerticalFlow(); + + //# Delta-scroll + var d = (isVerticalFlow) + ? value.y - data.LastScrollPosition.y + : value.x - data.LastScrollPosition.x; + + data.LastScrollPosition = value; + + /** + * We perform some checks here to detect if there is extremely fast scrolling happening. + * Fast scrolling like that will cause the ScrollPanel to "lose" the tiles in the recycling process. + * Instead of performing standard recycling, we check to see if the scroll delta is about the length + * of the scroll panel. If so, we simply get an index from our backing list using the value/max Scrollbar percentage, + * then restart the virtualization process (as far as placement goes) + */ + var isPaging = Mathf.Abs(d) > Height; + + if (isPaging && (VertScrollbar || HorzScrollbar)) + { + + var pct = (isVerticalFlow) + ? value.y / VertScrollbar.MaxValue + : value.x / HorzScrollbar.MaxValue; + + //# I've tried ceil and floor, but the results are kinda unpredictable sometimes. Rounding seems to be the best so far + //# without messing about with float tolerance. Mostly annoying after paging, and them touch scrolling back to 0; + //# sometimes the 0th tile is a tad off-y/x + var indexAtScrollPosition = Mathf.RoundToInt(pct * (list.Count - 1)); + + //# Restart the virtualization process. + Virtualize(list, indexAtScrollPosition); + + return; + + } + + //# Loop through all tiles seeing if they are out of bounds. If so we shift them to the top, or bottom + //# of the stack depending on which vertical bound they pass. + foreach (var tileInterface in tiles) + { + + int index = 0; + float newStart = 0; + var hasChangeOccured = false; + var panel = tileInterface.GetDfPanel(); + var start = (isVerticalFlow) ? panel.RelativePosition.y : panel.RelativePosition.x; + var panelLength = (isVerticalFlow) ? panel.Height : panel.Width; + var posExtreme = (isVerticalFlow) ? Height : Width; + + if (d > 0) + { + + if (!((start + panelLength) <= 0)) + { + continue; + } + + //# It would be great to just use Linq like this, but iOS throws AOT errors. + //# Any suggestions would be great. + //index = tiles.Max( x => x.GetListItemIndex() ) + 1; + //newStart = tiles.Max( x => x.GetDfPanel().RelativePosition.y ) + panelLength; + + //# Instead, we have to do it this way... + data.GetNewLimits(isVerticalFlow, true, out index, out newStart); + + //# Don't try to reposition this tile if there is no valid index in backing list. + if (!(index < list.Count)) + { + continue; + } + + hasChangeOccured = true; + panel.RelativePosition = (isVerticalFlow) + ? new Vector3(panel.RelativePosition.x, newStart + panelLength + padding.vertical) + : new Vector3(newStart + panelLength + padding.horizontal, panel.RelativePosition.y); + + } + else if (d < 0) + { + + if (!(start >= (posExtreme))) + { + continue; + } + + data.GetNewLimits(isVerticalFlow, false, out index, out newStart); + + if (index < 0) + { + continue; + } + + hasChangeOccured = true; + panel.RelativePosition = (isVerticalFlow) + ? new Vector3(panel.RelativePosition.x, newStart - (panelLength + padding.vertical)) + : new Vector3(newStart - (panelLength + padding.horizontal), panel.RelativePosition.y); + + } + + if (!hasChangeOccured) + { + continue; + } + + tileInterface.VirtualScrollItemIndex = index; + tileInterface.OnScrollPanelItemVirtualize(list[index]); + + } + + } + + /// + /// Initializes an information storage object that keeps track of various virtual scrolling + /// settings. + /// + /// + /// + /// + private dfVirtualScrollData initVirtualScrollData(IList backingList) + { + + var data = new dfVirtualScrollData { BackingList = backingList }; + virtualScrollData = data; + + return data; + + } + + /// + /// Instantiate, and add a tile to the scroll panel to be used for virtual scrolling. + /// + /// + /// + private IDFVirtualScrollingTile initTile(RectOffset padding) + { + + var inter = virtualScrollingTile.GetComponents() + .FirstOrDefault(p => p is IDFVirtualScrollingTile); + var tile = (IDFVirtualScrollingTile)Instantiate(inter); + var panel = tile.GetDfPanel(); + var isVerticalFlow = this.isVerticalFlow(); + + AddControl(panel); + + if (AutoFitVirtualTiles) + { + if (isVerticalFlow) + { + panel.Width = Width - padding.horizontal; + } + else + { + panel.Height = Height - padding.vertical; + } + } - var min = Vector2.one * float.MaxValue; - var max = Vector2.one * -float.MaxValue; + panel.RelativePosition = new Vector3(padding.left, padding.top); - for( int i = 0; i < controls.Count; i++ ) - { + return tile; - var child = controls[ i ]; + } - // Skip calculation of child controls that are not visible. - // NOTE: Only done during runtime, as this control is "live" - // in the editor and we don't want to change the layout - // during design time. Everything will be correct when running. - if( Application.isPlaying && !child.IsVisible ) - continue; + private bool isVerticalFlow() + { + return (FlowDirection == LayoutDirection.Vertical); + } - var controlMin = (Vector2)child.RelativePosition.CeilToInt(); - var controlMax = controlMin + child.Size.CeilToInt(); + #region Child control events - min = Vector2.Min( controlMin, min ); - max = Vector2.Max( controlMax, max ); + private void attachEvents(dfControl control) + { + control.IsVisibleChanged += childIsVisibleChanged; + control.PositionChanged += childControlInvalidated; + control.SizeChanged += childControlInvalidated; + control.ZOrderChanged += childOrderChanged; + } - } + private void detachEvents(dfControl control) + { + control.IsVisibleChanged -= childIsVisibleChanged; + control.PositionChanged -= childControlInvalidated; + control.SizeChanged -= childControlInvalidated; + control.ZOrderChanged -= childOrderChanged; + } - // If the minimum control position is greater than the origin, we'll need - // to compensate for that so that controls don't get moved to the origin - // and to allow for scrolling all the way to the far extents - var minOffset = Vector2.Max( Vector2.zero, min - new Vector2( scrollPadding.left, scrollPadding.top ) ); + void childOrderChanged(dfControl control, int value) + { + onChildControlInvalidatedLayout(); + } - // Regardless of where the current scroll position is, the - // max view extent is always the lower-right corner of the - // client rect. - max = Vector2.Max( max + minOffset, clientSize ); + void childIsVisibleChanged(dfControl control, bool value) + { + onChildControlInvalidatedLayout(); + } - return ( max - min ) + minOffset; + private void childControlInvalidated(dfControl control, Vector2 value) + { + onChildControlInvalidatedLayout(); + } - } + [HideInInspector] + private void onChildControlInvalidatedLayout() + { - [HideInInspector] - private void updateScrollbars() - { + if (scrolling || IsLayoutSuspended) + return; - var viewSize = calculateViewSize(); + if (autoLayout) + { + AutoArrange(); + } + + updateScrollbars(); - var clientSize = this.Size - new Vector2( scrollPadding.horizontal, scrollPadding.vertical ); + Invalidate(); - if( horzScroll != null ) - { - horzScroll.MinValue = 0; - horzScroll.MaxValue = viewSize.x; - horzScroll.ScrollSize = clientSize.x; - horzScroll.Value = Mathf.Max( 0, scrollPosition.x ); - } + } - if( vertScroll != null ) - { - vertScroll.MinValue = 0; - vertScroll.MaxValue = viewSize.y; - vertScroll.ScrollSize = clientSize.y; - vertScroll.Value = Mathf.Max( 0, scrollPosition.y ); - } - - } - - /// - /// This method triggers when the scroll position changes. It checks all of the tiles, and - /// performs recycling on them if they are outside the directional bounds. - /// - /// - /// - /// - private void virtualScrollPositionChanged( dfControl control, Vector2 value ) - { - - var data = GetVirtualScrollData(); - - if( data == null ) - { - return; - } - - var list = data.BackingList; - var padding = data.ItemPadding; - var tiles = data.Tiles; - var isVerticalFlow = this.isVerticalFlow(); - - //# Delta-scroll - var d = ( isVerticalFlow ) - ? value.y - data.LastScrollPosition.y - : value.x - data.LastScrollPosition.x; - - data.LastScrollPosition = value; - - /** - * We perform some checks here to detect if there is extremely fast scrolling happening. - * Fast scrolling like that will cause the ScrollPanel to "lose" the tiles in the recycling process. - * Instead of performing standard recycling, we check to see if the scroll delta is about the length - * of the scroll panel. If so, we simply get an index from our backing list using the value/max Scrollbar percentage, - * then restart the virtualization process (as far as placement goes) - */ - var isPaging = Mathf.Abs( d ) > Height; - - if( isPaging && ( VertScrollbar || HorzScrollbar ) ) - { + #endregion - var pct = ( isVerticalFlow ) - ? value.y / VertScrollbar.MaxValue - : value.x / HorzScrollbar.MaxValue; + #endregion - //# I've tried ceil and floor, but the results are kinda unpredictable sometimes. Rounding seems to be the best so far - //# without messing about with float tolerance. Mostly annoying after paging, and them touch scrolling back to 0; - //# sometimes the 0th tile is a tad off-y/x - var indexAtScrollPosition = Mathf.RoundToInt( pct * ( list.Count - 1 ) ); + /// + /// 自动跳到指定的index + /// + /// + /// + public dfPanel ScrollIntoIndex(int i) + { + if (isVerticalFlow()) + { + scrollPosition = new Vector2(0, i * lastWidth); + } + else + { + scrollPosition = new Vector2(i * lastWidth, 0); + } - //# Restart the virtualization process. - Virtualize( list, indexAtScrollPosition ); - return; - } + OnScrollPositionChanged(); - //# Loop through all tiles seeing if they are out of bounds. If so we shift them to the top, or bottom - //# of the stack depending on which vertical bound they pass. - foreach( var tileInterface in tiles ) - { - int index = 0; - float newStart = 0; - var hasChangeOccured = false; - var panel = tileInterface.GetDfPanel(); - var start = ( isVerticalFlow ) ? panel.RelativePosition.y : panel.RelativePosition.x; - var panelLength = ( isVerticalFlow ) ? panel.Height : panel.Width; - var posExtreme = ( isVerticalFlow ) ? Height : Width; - - if( d > 0 ) - { - - if( !( ( start + panelLength ) <= 0 ) ) - { - continue; - } - - //# It would be great to just use Linq like this, but iOS throws AOT errors. - //# Any suggestions would be great. - //index = tiles.Max( x => x.GetListItemIndex() ) + 1; - //newStart = tiles.Max( x => x.GetDfPanel().RelativePosition.y ) + panelLength; - - //# Instead, we have to do it this way... - data.GetNewLimits( isVerticalFlow, true, out index, out newStart ); - - //# Don't try to reposition this tile if there is no valid index in backing list. - if( !( index < list.Count ) ) - { - continue; - } - - hasChangeOccured = true; - panel.RelativePosition = ( isVerticalFlow ) - ? new Vector3( panel.RelativePosition.x, newStart + panelLength + padding.vertical ) - : new Vector3( newStart + panelLength + padding.horizontal, panel.RelativePosition.y ); - - } - else if( d < 0 ) - { - - if( !( start >= ( posExtreme ) ) ) - { - continue; - } - - data.GetNewLimits( isVerticalFlow, false, out index, out newStart ); - - if( index < 0 ) - { - continue; - } - - hasChangeOccured = true; - panel.RelativePosition = ( isVerticalFlow ) - ? new Vector3( panel.RelativePosition.x, newStart - ( panelLength + padding.vertical ) ) - : new Vector3( newStart - ( panelLength + padding.horizontal ), panel.RelativePosition.y ); - - } - - if( !hasChangeOccured ) - { - continue; - } - - tileInterface.VirtualScrollItemIndex = index; - tileInterface.OnScrollPanelItemVirtualize( list[ index ] ); - - } - - } - - /// - /// Initializes an information storage object that keeps track of various virtual scrolling - /// settings. - /// - /// - /// - /// - private dfVirtualScrollData initVirtualScrollData( IList backingList ) - { - - var data = new dfVirtualScrollData { BackingList = backingList }; - virtualScrollData = data; - - return data; - - } - - /// - /// Instantiate, and add a tile to the scroll panel to be used for virtual scrolling. - /// - /// - /// - private IDFVirtualScrollingTile initTile( RectOffset padding ) - { - - var inter = virtualScrollingTile.GetComponents() - .FirstOrDefault( p => p is IDFVirtualScrollingTile ); - var tile = (IDFVirtualScrollingTile)Instantiate( inter ); - var panel = tile.GetDfPanel(); - var isVerticalFlow = this.isVerticalFlow(); - - AddControl( panel ); - - if ( AutoFitVirtualTiles ) - { - if ( isVerticalFlow ) - { - panel.Width = Width - padding.horizontal; - } else - { - panel.Height = Height - padding.vertical; - } - } - - panel.RelativePosition = new Vector3( padding.left, padding.top ); - - return tile; - - } - - private bool isVerticalFlow() - { - return ( FlowDirection == LayoutDirection.Vertical ); - } - - #region Child control events - - private void attachEvents( dfControl control ) - { - control.IsVisibleChanged += childIsVisibleChanged; - control.PositionChanged += childControlInvalidated; - control.SizeChanged += childControlInvalidated; - control.ZOrderChanged += childOrderChanged; - } - - private void detachEvents( dfControl control ) - { - control.IsVisibleChanged -= childIsVisibleChanged; - control.PositionChanged -= childControlInvalidated; - control.SizeChanged -= childControlInvalidated; - control.ZOrderChanged -= childOrderChanged; - } - - void childOrderChanged( dfControl control, int value ) - { - onChildControlInvalidatedLayout(); - } - - void childIsVisibleChanged( dfControl control, bool value ) - { - onChildControlInvalidatedLayout(); - } - - private void childControlInvalidated( dfControl control, Vector2 value ) - { - onChildControlInvalidatedLayout(); - } - - [HideInInspector] - private void onChildControlInvalidatedLayout() - { - - if( scrolling || IsLayoutSuspended ) - return; - - if( autoLayout ) - { - AutoArrange(); - } + List tiels = + (List)virtualScrollData.GetType().GetField("Tiles").GetValue(virtualScrollData); - updateScrollbars(); - Invalidate(); - } + foreach (IDFVirtualScrollingTile scrollingTile in tiels) + { + if (scrollingTile.VirtualScrollItemIndex == i) + { - #endregion + var panel = scrollingTile.GetDfPanel(); + return panel; + } + } - #endregion + return null; + } }