MoDu MoDu - 9 days ago 6
C# Question

Infinite scrolling Flipview updating content while flipping

I'm trying to implement a calendar style (date based) infinite

FlipView
where the user can scroll with touch. I'm binding the
FlipView
's
ItemTemplate
with a custom
ObservableCollection
. Everything is showing up nicely and I'm manipulating the
ObservableCollection
to provide the desired behaviour: when selected index is changed, add new element to top and remove from the bottom.

private void OnIndexChanged(object sender, SelectionChangedEventArgs e)
{
//Works great on slow swiping with pauses, no offset artifacts
DataGroup.OnIndexChanged(GroupFlip.SelectedIndex);
}


Problem is,
FlipView
will only trigger the
SelectedIndex
changed event when the user stops scrolling. For small swiping, this is fine, but the user can easily reach the end of the collection and come to a dead end until the collection is updated.

I've successfully subscribed to the
FlipView
inner
ScrollViewer
's
[Viewchanged][1]
, as suggested here and am able to see and use the
HorizontalOffset
to calculate the new index offset and manipulate the collection.

Problem is, when the collection is manipulated in this event,
FlipView
jumps around in various ways, depending on the type of manipulation of the user and collection.

InnerFlipper.ViewChanged += OnSoftScroll;//InnerFlipper is caught from the `VisualHelperTree`
private void OnSoftScroll(object sender, ScrollViewerViewChangedEventArgs e)
{
(...)
double UpperMargin = ScrollableSize - Offset;//Value is reliable
if (UpperMargin < ViewObservableSize)
{
DataGroup.OnIndexDelta(1);
}
(...)
}


I've tried many combinations of ignoring some events to avoid double triggering, forcing the new
HorizontalOffset
to a calculated value based on the index change and curre offset, etc. None gives a transparent result, which is a seamless infinite scroll.

Any ideas how to avoid artifacts, handle this event or even other ways to implement to achieve the desired outcome?

Answer

Finally solved this by completely rebuilding the way the FlipView works. If the FlipView is initialized with a really large "virtual" set (i.e. no content), then all I have to do when scrolling is update the content, not messing with the FlipView's index or item count.

Hope it helps anyone else.

EDIT:

I made a code snippet from the implementation. However, looking back it just begs to also use a recyclable pattern, to prevent massive GC when scrolling a lot. The concept of the large virtual list that updates still stands. I am using general objects because my view switched the type of custom control each page had (week page, month page, etc). Hope it helps you guys, happy coding.

On the control side, we have a FlipView with just the Loaded event subscribed.

    protected ScrollViewer InnerScroller;

    private void OnFlipViewerLoaded(object sender, RoutedEventArgs e)
    {
        InnerFlipper = (ScrollViewer)FindChildControl<ScrollViewer>(sender);
        InnerFlipper.ViewChanged += OnPageScroll;
    }

    /// <summary>
    /// Our custom pseudo-infinite collection
    /// </summary>
    ModelCollection ItemsCollection = new ModelCollection();

    private void OnPageScroll(object sender, ScrollViewerViewChangedEventArgs e)
    {
        InnerFlipper.ViewChanged -= OnPageScroll;//Temporarily stop handling this event, to prevent double triggers and let the CPU breath for a little

       int FlipViewerRealIndex = GetFlipViewIndex(sender);

        ItemsCollection.UpdatePages(FlipViewerRealIndex);

        InnerFlipper.ViewChanged += OnPageScroll;//Start getting this event again, ready for the next iteration
    }

    /// <summary>
    /// No idea why, FlipView's inner offset starts at 2. Fuck it, subtract 2 and it works fine.
    /// </summary>
    /// <param name="sender"></param>
    /// <returns></returns>
    public static int GetFlipViewIndex(object sender)
    {
        double CorrectedScrollOffset= ((ScrollViewer)sender).HorizontalOffset - 2;
        int NewIndex = (int)Math.Round(CorrectedScrollOffset);//Round instead of simple cast, otherwise there is a bias in the direction

        return NewIndex;
    }

On the model collection setup we have.

    private const int VirtualListRadius = 1000;
    /// <summary>
    /// The collection constructor, runs the first step of the data filling.
    /// </summary>
    public ModelCollection()
    {
        //Fill in the virtual list with the default (mostly null) custom control.
        for (int i = 1; i <= VirtualListRadius; i++)
        {
            object LeftMostPage = NewPageControl(args1);
            object RightMostPage = NewPageControl(args2);
            Items.Insert(0, LeftMostPage);
            Items.Add(RightMostPage);
        }
    }        

    /// <summary>
    /// The FlipViewer's items list, with all the virtual content and real content (where applicable)
    /// </summary>
    public ObservableCollection<Object> Items
    {
        get { return _items; }
        set { SetProperty(ref _items, value); }
    }
    public ObservableCollection<Object> _items = new ObservableCollection<Object>();

The code to update the pages:

/// <summary>
    /// How many pages of content should be buffered in each direction
    /// </summary>
    private const int ObservableListRadius = 3;

    /// <summary>
    /// The main update function that replaces placeholder-virtual content with actual content, while freeing up content that's no longe necessary
    /// </summary>
    /// <param name="scrollIndex">The new index absolute index that should be extracted from the Flipview's inner scroller</param>
    public void UpdatePages(int scrollIndex)
    {
        if (scrollIndex < 0 || scrollIndex > Items.Count - 1)
        {
            //If the scroll has move beyond the virtual list, then we're in trouble
            throw new Exception("The scroll has move beyond the virtual list");
        }

        int MinIndex = Math.Max(scrollIndex - ObservableListRadius, 0);
        int MaxIndex = Math.Min(scrollIndex + ObservableListRadius, Items.Count() - 1);

        //Update index content
        (Items.ElementAt(scrollIndex) as ModelPage).UpdatePage(args1);

        Status = Enumerators.CollectionStatusType.FirstPageLoaded;

        //Update increasing radius indexes
        for (int radius = 1; radius <= Constants.ObservableListRadius; radius++)
        {
            if (scrollIndex + radius <= MaxIndex && scrollIndex + radius > MinIndex)
            {
                (Items.ElementAt(scrollIndex + radius) as ModelPage).UpdatePage(args2);
            }

            if (scrollIndex - radius >= MinIndex && scrollIndex - radius <= MaxIndex)
            {
                (Items.ElementAt(scrollIndex - radius) as ModelPage).UpdatePage(args3);
            }

        }     
    }