Search code examples
iosxamarin.iosuicollectionviewuiviewanimationuipagecontrol

Custom animation on SetContentOffset for IOs CollectionView triggered by PageControl is not smooth


I have a Xamarin.IOs project. I'm using a UICollectionView with Paging enabled. Then there's a UIPageControl that's in sync with the UICollectionView.

There are two functionalities required,

  1. On swiping the UICollectionView, the current page of the CollectionView would change and the current indicator of the UIPageControl would also change in synchronization.

    Implementation:

    In UICollectionViewSource,

    public override void DecelerationEnded(UIScrollView scrollView)
    {
        Index = (int)(scrollView.ContentOffset.X / scrollView.Frame.Width);
        PageControl.CurrentPage = Index;
        CardSwiped?.Invoke(this, new CustomEventArgs(Index));
    }
    

    In the relevant ViewController,

    private void SomeMethod()
    {
        CollectionSource.CardSwiped += OnCardSwiped;
    }
    
    private void OnCardSwiped(object sender, CustomEventArgs args)
    {
        // Perform some action
    }
    

    Works Fine!

  2. On tapping the UIPageControl, the CollectionView current page should change according to the tap and the current indicator of the UIPageControl would also change in synchronization.

    Implementation:

    In the relevant ViewController,

    private void SomeMethod()
    {
        PageControl.PrimaryActionTriggered += OnPageControlTapped;
    }
    
    private void OnPageControlTapped(object sender, EventArgs args)
    {
        // Some action
    
        var pageControlIndex = CardPageControl.CurrentPage;
        var sourceIndex = CollectionSource.Index;
        var distanceToScroll = CardCollectionView.Frame.Width * (pageControlIndex - sourceIndex);
    
        UIView.Animate(0.5, 0, UIViewAnimationOptions.TransitionNone,
            () =>
            {
                CollectionView.SetContentOffset(
                    new CGPoint(CollectionView.ContentOffset.X + distanceToScroll, CollectionView.ContentOffset.Y),
                    false);
            },
            () =>
            {
                CollectionSource.DecelerationEnded(CollectionView);
            });
    }
    

    I can't invoke SetContentOffset with animation true,

    CollectionView.SetContentOffset(new CGPoint(CollectionView.ContentOffset.X + y, CollectionView.ContentOffset.Y), true);
    

    because I need to invoke CollectionSource.DecelerationEnded(CollectionView); after the page is positioned by the SetContentOffset method.

    The issue is, when I use this custom animation, the CollectionView is not animated correctly,

    enter image description here

How can I overcome this?


Solution

  • There seems to be some issue with how UICollectionView scrolls for SetContentOffset with custom animation while Paging is enabled.

    When the full page width (CardCollectionView.Frame.Width) is taken as the distance to be scrolled,

    var distanceToScroll = CardCollectionView.Frame.Width * (pageControlIndex - sourceIndex);
    

    and used in SetContentOffset,

    CollectionView.SetContentOffset(
                new CGPoint(CollectionView.ContentOffset.X + distanceToScroll, CollectionView.ContentOffset.Y),
                false);
    

    then it won't animate the way it would with custom animations.

    Solution:

    You should not custom animate for the full page width scroll distance, instead you should reduce the scroll distance by 1 pixel (choosing 1 pixel to make it less notable, any other amount would work too) from the full page width. That is,

    var distanceToScroll = (CardCollectionView.Frame.Width - 1) * (pageControlIndex - sourceIndex);
    

    The -1 in CardCollectionView.Frame.Width - 1 is the so called workaround.

    Then you have to address this reduction after the animation. That is, in the UIView.Animate code above, the call back function should have one additional line,

    CardCollectionView.SetContentOffset(
        new CGPoint(CardCollectionView.ContentOffset.X + (pageControlIndex - sourceIndex), CardCollectionView.ContentOffset.Y), 
        false);
    

    Then the UIView.Animate would look like,

    UIView.Animate(0.5, 0, UIViewAnimationOptions.TransitionNone,
        () =>
        {
            CollectionView.SetContentOffset(
                new CGPoint(CollectionView.ContentOffset.X + distanceToScroll, CollectionView.ContentOffset.Y),
                false);
        },
        () =>
        {
            CardCollectionView.SetContentOffset(
                new CGPoint(CardCollectionView.ContentOffset.X + (pageControlIndex - sourceIndex), CardCollectionView.ContentOffset.Y), 
                false);
            CollectionSource.DecelerationEnded(CollectionView);
        });
    

    Now the custom animation would work really smooth 🙂