Skip to content
This repository has been archived by the owner on May 18, 2021. It is now read-only.

Commit

Permalink
Scroll strategy changed.
Browse files Browse the repository at this point in the history
.gitignore now ignores debug outputs.
  • Loading branch information
karno committed Jan 9, 2015
1 parent 2ab80f6 commit 7820b92
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 73 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -23,6 +23,7 @@ Thumbs.db
[Oo]bj/
*.lib
*.sbr
[Dd]ebug*/
[Rr]elease*/
_ReSharper*/
[Tt]est[Rr]esult*/
Expand Down
158 changes: 86 additions & 72 deletions StarryEyes/Views/Behaviors/TimelineScrollLockerBehavior.cs
Expand Up @@ -18,7 +18,9 @@ namespace StarryEyes.Views.Behaviors
{
public class TimelineScrollLockerBehavior : Behavior<ScrollViewer>
{
private const double TopLockThreshold = 0.9;
private const double ScrollEpsilonPixel = 10.0;
private const double ScrollEpsilonItem = 1.0;


private readonly CompositeDisposable _disposables = new CompositeDisposable();

Expand All @@ -38,8 +40,8 @@ public bool IsScrollLockEnabled
/// Dependency property for flag of scroll lock is enabled or disabled.
/// </summary>
public static readonly DependencyProperty IsScrollLockEnabledProperty =
DependencyProperty.Register("IsScrollLockEnabled", typeof(bool),
typeof(TimelineScrollLockerBehavior), new PropertyMetadata(false));
DependencyProperty.Register("IsScrollLockEnabled", typeof(bool), typeof(TimelineScrollLockerBehavior),
new PropertyMetadata(false));

/// <summary>
/// Flag of scroll lock is enabled only scroll position is not zero. <para />
Expand Down Expand Up @@ -72,11 +74,11 @@ public IList ItemsSource
/// </summary>
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IList), typeof(TimelineScrollLockerBehavior),
new PropertyMetadata(null, ItemsSourceChanged));
new PropertyMetadata(null, ItemsSourceChanged));

private static void ItemsSourceChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
private static void ItemsSourceChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
var behavior = dependencyObject as TimelineScrollLockerBehavior;
var behavior = source as TimelineScrollLockerBehavior;
// sender is ScollLockerBehavior and that is attached.
if (behavior != null && behavior.AssociatedObject != null)
{
Expand All @@ -98,19 +100,36 @@ public bool IsAnimationEnabled
/// Dependency property for flag of scroll animation is enabled or disabled.
/// </summary>
public static readonly DependencyProperty IsAnimationEnabledProperty =
DependencyProperty.Register("IsAnimationEnabled", typeof(bool), typeof(TimelineScrollLockerBehavior), new PropertyMetadata(true));
DependencyProperty.Register("IsAnimationEnabled", typeof(bool), typeof(TimelineScrollLockerBehavior),
new PropertyMetadata(true));

/// <summary>
/// Current applied scroll unit
/// </summary>
public ScrollUnit CurrentScrollUnit
{
get { return (ScrollUnit)GetValue(CurrentScrollUnitProperty); }
set { SetValue(CurrentScrollUnitProperty, value); }
}

/// <summary>
/// Dependency property for current scroll unit
/// </summary>
public static readonly DependencyProperty CurrentScrollUnitProperty =
DependencyProperty.Register("CurrentScrollUnit", typeof(ScrollUnit), typeof(TimelineScrollLockerBehavior),
new PropertyMetadata(ScrollUnit.Pixel));


// internal caches
private readonly Queue<double> _scrollOffsetQueue = new Queue<double>();
private readonly Timer _scrollTimer;
private double _lastScrollOffset;
private readonly LinkedList<double> _lastScrollOffsets = new LinkedList<double>();
private int _previousItemCount;
private IDisposable _itemSourceCollectionChangeListener;
// ✨ the magic ✨
private volatile bool _magicIgnoreUserScrollOnce;
private volatile bool _isMagicIgnoreHolded;
// wait for dispatcher
private volatile bool _isDispatcherAffected;
// scroll animation synchronize latch
private volatile bool _isScrollAnimating;

public TimelineScrollLockerBehavior()
{
Expand Down Expand Up @@ -172,6 +191,13 @@ private void ScrollChanged(ScrollChangedEventArgs e)
var itemCount = source.Count;
var verticalOffset = e.VerticalOffset;

var epsilon = ScrollEpsilonItem;
if (this.CurrentScrollUnit == ScrollUnit.Pixel)
{
epsilon = ScrollEpsilonPixel;
}


// ***** check item is added or not *****

// check and update items count latch
Expand All @@ -184,14 +210,18 @@ private void ScrollChanged(ScrollChangedEventArgs e)
// if scroll extent is not changed or shrinked, nothing to do.
if (e.ExtentHeightChange <= 0)
{
_lastScrollOffset = verticalOffset;
_lastScrollOffsets.AddFirst(verticalOffset);
return;
}

// calculate position should scroll to.
// e.VerticalOffset indicates illegal offset when timeline is invisible.
// var prevPosition = e.VerticalOffset + e.ExtentHeightChange;
var prevPosition = _lastScrollOffset + e.ExtentHeightChange;

var firstNode = _lastScrollOffsets.First;
var lastScrollOffset = firstNode != null ? firstNode.Value : 0;

var prevPosition = lastScrollOffset + e.ExtentHeightChange;
if (prevPosition > e.ExtentHeight)
{
// too large to scroll -> use value from e.VerticalOffset
Expand All @@ -202,7 +232,7 @@ private void ScrollChanged(ScrollChangedEventArgs e)

// ScrollLock AND !ScrollLockOnlyScrolled -> Lock
// ScrollLock AND ScrollLockOnlyScrolled AND _lastScrollOffset is not Zero -> Lock
if (IsScrollLockEnabled && (!IsScrollLockOnlyScrolled || _lastScrollOffset > TopLockThreshold))
if (IsScrollLockEnabled && (!IsScrollLockOnlyScrolled || lastScrollOffset > epsilon))
{
if (IsScrollLockOnlyScrolled)
{
Expand All @@ -212,8 +242,8 @@ private void ScrollChanged(ScrollChangedEventArgs e)
if (_scrollOffsetQueue.Count > 0)
{
System.Diagnostics.Debug.WriteLine("* Currently scrolling -> update animation.");
// start animation with a ✨ magic ✨
this.RunAnimation(prevPosition, true);
// start animation
this.RunAnimation(prevPosition);
return;
}
}
Expand All @@ -231,58 +261,54 @@ private void ScrollChanged(ScrollChangedEventArgs e)
else
{
// simply update last offset.
_lastScrollOffset = verticalOffset;
_lastScrollOffsets.Clear();
_lastScrollOffsets.AddFirst(verticalOffset);
}
return;
}

// ***** check is user scrolled or not ****

// _lastScrollOffset != e.VerticalOffset
if (Math.Abs(_lastScrollOffset - verticalOffset) > TopLockThreshold)
if (_isScrollAnimating)
{
if (_magicIgnoreUserScrollOnce)
{
// magic ignore once
if (!_isMagicIgnoreHolded)
{
_magicIgnoreUserScrollOnce = false;
System.Diagnostics.Debug.WriteLine(
"✨ MAGICAL IGNORE [RELEASE] ✨" +
" LSO:" + _lastScrollOffset + " / VO: " + verticalOffset);
}
else
{
System.Diagnostics.Debug.WriteLine(
"✨ MAGICAL IGNORE [HOLD] ✨" +
" LSO:" + _lastScrollOffset + " / VO: " + verticalOffset);
}
_lastScrollOffset = verticalOffset;
return;
}
System.Diagnostics.Debug.WriteLine(
"* User scroll detected." +
" LSO: " + _lastScrollOffset + " / VO: " + verticalOffset);
// scrolled by user?
// -> abort animation, exit immediately
lock (_scrollOffsetQueue)
// lastScrollOffset != e.VerticalOffset
while (_lastScrollOffsets.Count > 0)
{
if (_scrollOffsetQueue.Count > 0)
var offset = _lastScrollOffsets.Last.Value;
System.Diagnostics.Debug.WriteLine("% DQLO: " + offset + " / " + verticalOffset + ", eps: " + epsilon);
if (Math.Abs(offset - verticalOffset) < epsilon)
{
System.Diagnostics.Debug.WriteLine(" -> Scroll stopped by user-interaction.");
_scrollOffsetQueue.Clear();
System.Diagnostics.Debug.WriteLine("% Check.");
// scrolled by animation
if (offset < 1)
{
System.Diagnostics.Debug.WriteLine("% finished scrolling.");
_isScrollAnimating = false;
}
return;
}
_lastScrollOffsets.RemoveLast();
}
// write back last scroll offset and item count
_lastScrollOffset = verticalOffset;
_previousItemCount = itemCount;
return;
_isScrollAnimating = false;
}

// ***** or maybe caused by scroll animation *****
// -> do nothing.
// scroll by user-interaction

return;
System.Diagnostics.Debug.WriteLine("* User scroll detected.");

// scrolled by user?
// -> abort animation, exit immediately
lock (_scrollOffsetQueue)
{
if (_scrollOffsetQueue.Count > 0)
{
System.Diagnostics.Debug.WriteLine(" -> Scroll stopped by user-interaction.");
_scrollOffsetQueue.Clear();
}
}
// write back last scroll offset and item count
_lastScrollOffsets.AddFirst(verticalOffset);
_previousItemCount = itemCount;
}

/// <summary>
Expand Down Expand Up @@ -347,29 +373,22 @@ private IDisposable ListenCollectionChange(IList source)
/// Run scroll animation (to position 0).
/// </summary>
/// <param name="offset">beginning scroll offset</param>
/// <param name="setMagicalIgnore">set magical ignore flag</param>
private void RunAnimation(double offset, bool setMagicalIgnore = false)
private void RunAnimation(double offset)
{
// clear old animation queue
lock (_scrollOffsetQueue)
{
_scrollOffsetQueue.Clear();
}

System.Diagnostics.Debug.WriteLine("# New animation started. VO: " + offset);

if (setMagicalIgnore)
{
this._isMagicIgnoreHolded = true;
_magicIgnoreUserScrollOnce = true;
}
this._isScrollAnimating = true;

// scroll to initial position (enforced)
ScrollToVerticalOffset(offset);
// above method should not call via dispatcher. It will causes flickers in timeline.

System.Diagnostics.Debug.WriteLine("# Registering new animation.");

System.Diagnostics.Debug.WriteLine("# Registering new animation. (magic ignore?" + setMagicalIgnore + ")");
// create animation
// callback method is called every 10 milliseconds.
// scroll is should be completed in 60 msec.
Expand All @@ -384,7 +403,6 @@ private void RunAnimation(double offset, bool setMagicalIgnore = false)
_scrollOffsetQueue.Enqueue(0);
RunScrollTimer();
}
this._isMagicIgnoreHolded = false;
}

private void RunScrollTimer()
Expand Down Expand Up @@ -416,13 +434,8 @@ private void TimerCallback()
if (_scrollOffsetQueue.Count == 0)
{
StopScrollTimer();
System.Diagnostics.Debug.WriteLine("# Scroll completed. [magic ignore hold?" + _isMagicIgnoreHolded + "]");
System.Diagnostics.Debug.WriteLine("# Scroll completed.");
// disable magical ignore
if (!this._isMagicIgnoreHolded)
{
this._magicIgnoreUserScrollOnce = false;
}

return;
}
dequeuedOffset = this._scrollOffsetQueue.Dequeue();
Expand Down Expand Up @@ -456,13 +469,14 @@ private void TimerCallback()
private void ScrollToVerticalOffset(double offset, [CallerMemberName]string caller = "[not provided]")
{
System.Diagnostics.Debug.WriteLine("# Scroll to: " + offset + " by " + caller);
this._lastScrollOffset = offset;
if (offset < 1)
{
_lastScrollOffsets.AddFirst(0);
this.AssociatedObject.ScrollToTop();
}
else
{
_lastScrollOffsets.AddFirst(offset);
this.AssociatedObject.ScrollToVerticalOffset(offset);
}
}
Expand Down
3 changes: 2 additions & 1 deletion StarryEyes/Views/WindowParts/Primitives/Timeline.xaml
Expand Up @@ -2173,7 +2173,8 @@
Mode=OneWayToSource}"
IsScrollOnTop="{Binding IsScrollOnTop,
Mode=OneWayToSource}" />
<behaviors:TimelineScrollLockerBehavior IsAnimationEnabled="{Binding IsAnimationEnabled,
<behaviors:TimelineScrollLockerBehavior CurrentScrollUnit="{Binding ScrollUnit}"
IsAnimationEnabled="{Binding IsAnimationEnabled,
Mode=OneWay}"
IsScrollLockEnabled="{Binding IsScrollLockEnabled,
Mode=OneWay}"
Expand Down

0 comments on commit 7820b92

Please sign in to comment.